---
slug: "molnify-app-builder-skill"
source_type: "skill_md"
source_url: "https://cdn.jsdelivr.net/gh/molnify/molnify-app-builder-skill@main/SKILL.md"
repo: "https://github.com/molnify/molnify-app-builder-skill"
source_file: "SKILL.md"
branch: "main"
---
---
name: molnify-app-builder
description: "Build, convert, validate, and style Molnify apps: spreadsheet-driven web applications where Excel or Google Sheets formulas drive the logic and colored cells define inputs, outputs, charts, and actions. Use when creating a Molnify app from scratch, converting an existing spreadsheet into one, validating or styling an app, or answering questions about how Molnify apps work."
license: Apache-2.0
metadata:
  version: 1.0.13
---

# Molnify App Development Guide

This is the primary reference for building Molnify applications.

## Overview

Molnify apps are **spreadsheet-driven calculation applications**. You design your app in Excel or Google Sheets, where:
- **Green cells** = Inputs (user-provided parameters)
- **Red cells** = Outputs (calculated results)
- **Blue cells** = Charts and Tables (aggregated data visualizations)
- **Yellow cells** = Actions (automated operations like email, HTTP calls)
- **Purple cells** = Metadata (app settings and configuration)

The spreadsheet formulas perform all calculations. Molnify renders the UI and handles user interaction.

Molnify reads the Excel file **left to right, top to bottom**. The order cells appear determines the order in the app UI.

### Cell Structure

**Inputs, Outputs, Charts** require **three consecutive cells**:
1. **Title cell** - The label shown to users
2. **Value cell** (colored) - The actual value/formula
3. **UI cell** - Configuration options (can be empty)

**Metadata** requires **two consecutive cells, both colored purple**:
1. **Key cell** (purple) - The metadata property name (e.g., "Name", "ID")
2. **Value cell** (purple) - The metadata value

**Actions** require **pairs of cells per property, value cell colored yellow**:
1. **Key cell** (no color) - The property name (e.g., "type", "to", "subject")
2. **Value cell** (yellow) - The property value

Each action is a contiguous block of these pairs, separated from other actions by a blank row.

### Quick Start

Most common patterns at a glance:

| Task | How |
|------|-----|
| Create a new app from scratch | Use `AppBuilder` - see `creating-from-scratch.md` |
| Convert an existing Excel file | Follow `converting-excel.md` step-by-step |
| Add an input | Green cell (`#00B050`), title left, UI options right |
| Add an output | Red cell (`#FF0000`), formula referencing model or inputs |
| Add a chart | Blue data cells (`#0070C0`), headers/labels uncolored |
| Add an action | Yellow cell pairs (`#FFFF00`), key left, value right |
| Add metadata | Purple cell pairs (`#7030A0`), first sheet only |
| Style the app | CSS metadata + `styling.md` recipes |

**Workflow:** Define → Build (AppBuilder or openpyxl) → Validate → Deploy

### Best Practices

1. **Always set the `ID` metadata** - This gives your app a stable URL and ensures you keep access to database tables when updating the app. Without an ID, Molnify auto-generates one, and updating creates a copy instead of replacing.

2. **Only include metadata that differs from defaults** - Do not add metadata cells for values that match the default. This keeps your spreadsheet clean and easier to maintain. For example, don't include `AutoCalcEnabled: TRUE` since TRUE is already the default.

3. **Metadata must be on the first sheet** - All purple metadata cells must be placed on the first sheet of your workbook. Metadata on other sheets will be ignored.

4. **Set `ParseAllSheets: TRUE` for multi-sheet apps** - By default, Molnify only parses the first sheet. If your app has inputs, outputs, actions, charts or metadata on multiple sheets, you must set the metadata `ParseAllSheets` to `TRUE`. AppBuilder always adds this automatically.

5. **Use `molnifyIgnore` to exclude sheets from color interpretation** - Place the text `molnifyIgnore` in cell A1 of any sheet that should not be scanned for Molnify components. If A1 already contains data, insert a new row at the top first. This is essential for sheets containing raw data models, lookup tables, or any content with conditional formatting or incidental cell colors that could be misinterpreted as inputs/outputs.

6. **Ensure text is readable on its background** - When customizing colors (buttons, panels, headers), always verify sufficient contrast between text and background. Light text on light backgrounds or dark text on dark backgrounds makes the app unusable.

7. **Display numbers at a sensible scale and precision** - Raw formula results are rarely display-ready: they carry excessive decimals (`33.333333333`) *and* often an awkward magnitude. Two separate decisions:
   - **Precision:** Round to what the reader can act on. ~3-4 significant figures is plenty for most displays. Use Excel's `ROUND()` in the formula, or the `decimals=N` UI option (and `axisDecimals=N` for chart axes - long axis tick labels are a common readability killer).
   - **Magnitude:** If values run in the thousands or millions, scale the unit instead of printing every digit. `124 500 SEK` reads better as `124.5 tSEK`, and `8 300 000 SEK` as `8.3 MSEK`. Divide by 1000/1e6 in the formula (or use the `multiplier` UI option) and state the scaled unit in the title - e.g. title "Revenue (tSEK)" rather than a 7-digit axis. Keep the chosen unit consistent across related outputs and a chart's whole axis.
   - **Rule of thumb:** before finalizing, ask "at this scale, is this number easy to read at a glance?" An axis labelled `0, 25000, 50000, 75000` should usually be `0, 25, 50, 75` with a "tSEK" axis title. This applies to scalar outputs, table cells, and chart axes alike.

8. **Verify formulas after adding or removing cells** - When inserting or deleting rows/columns, Excel formulas may shift or break. Always verify that all formulas still reference the correct cells after structural changes. This is especially critical when working programmatically (e.g., with openpyxl) where automatic reference adjustment doesn't occur.

9. **Keep strings in formulas under 256 characters** - Excel automatically inserts `_LONGTEXT` references when strings exceed 256 characters, but Molnify does not support `_LONGTEXT`. Split long strings using `&` or `CONCATENATE()`:
   ```
   ❌ ="This is a very long string that exceeds 256 characters..."
   ✓ ="This is a very long string that"&" has been split into chunks"&" each under 256 characters"
   ```

10. **Keep calculations in the spreadsheet, not in JavaScript** - Molnify's strength is that business logic lives in Excel formulas. Avoid offloading calculations, conditional logic, or data transformations to JavaScript. JS should be reserved for UI manipulation (DOM changes, styling, event handling), not for computing values that formulas can handle. This keeps the app auditable, testable, and maintainable.

11. **COUNTA counts empty strings from autofill** - When autofill populates a named range, cells that receive no data are cleared but hold the value `""` (empty string). `COUNTA` counts these as non-empty. Use `COUNTIF(range,"<>")` or `SUMPRODUCT((range<>"")*1)` instead to count only cells with actual data.

12. **Design with intent, not with effects** - Every app should look like it was designed for its purpose, not generated from a template. Pick a color palette that fits the domain. Choose fonts with character. Avoid generic AI-generated aesthetics: purple gradients on white, uniform rounded corners and card shadows on every element, pulsing animations, gradient text, emoji placeholders, and staggered fade-up animations. Left-aligned content is easier to scan than centered. Vary visual weight instead of making everything look the same. If it looks like every other AI-generated dashboard, it needs a point of view.

13. **Validate the *assembled* string when building formulas in code** - Concatenating formula text in Python (especially adjacent f-strings) easily produces a stray `""` that Excel reads as an *escaped quote*, swallowing every following `&` operator and string into one literal. A corrupted formula doesn't just fail its own cell: on load Excel runs "file level validation and repair" and **strips the entire formula table for that sheet** rather than dropping the one bad formula - so the app can render with every formula gone. Adjacent pieces like `f'="<div>…"'` + `f'"&"<h2>…"'` concatenate to `="<div>…""&"<h2>…"`, where the `""` is misread. After building a formula programmatically, `print`/`assert` the final string and confirm the double-quotes are balanced and every `"&"` is a real operator, not buried inside a literal. `molnify_validate.py` flags formulas with an odd number of double-quotes, which catches the common form of this bug.

### CSS & DOM Quick Reference

The rendered DOM does NOT match the spreadsheet - inputs are wrapped in third-party widgets and the selectors differ. `styling.md` has the full DOM tree, selector reference, default color scheme, chart-series colors, the Select2 / Font Awesome / custom-header gotchas, CSS Grid layouts, and dashboard recipes.

### Companion Guides

Alongside this reference, companion guides cover specific needs: `creating-from-scratch.md`, `converting-excel.md`, `styling.md`, `styling-examples.md`, `design.md`, `patterns.md`, `database.md`, `report-templates.md`, `custom-frontend.md`, `advanced-topics.md`, `python.md`, and `examples/`. See `local-instructions.md` for an index of what each covers and when to reach for it, plus local setup and deployment.

### Choosing Your UI Approach

| What you want | Approach | Guide |
|---------------|----------|-------|
| Colors, fonts, spacing | CSS metadata + the CSS & DOM quick ref above | `design.md` |
| Rearranged layout, dashboard, CSS Grid | CSS + `JavaScriptAfterLoad` | `styling.md` |
| Wizard, master-detail, conditional sections | Standard Molnify features | `patterns.md` |
| Fully custom DOM | Headless mode (`Headless: TRUE`) | `custom-frontend.md` |

**Many "modern" or "professional" requests are met by the second row** - restyling and rearranging the default UI is usually the quickest path. When a design needs a layout or interaction the default UI can't express, a custom (headless) frontend is a fully supported tool - reach for it whenever it fits the goal.

---

## Inputs Reference

**IMPORTANT: Input formulas are only calculated once when the app opens.** They are NOT recalculated during app use. This means:
- Inputs with formulas act as static default values
- Any cell that depends on user input (intermediate calculations, derived values) **must be an output (red cell)**, not an input
- Only outputs are recalculated when users change values

### Basic Properties
| Property | Description |
|----------|-------------|
| `cell` | Excel cell reference (e.g., `Sheet1!B2`) |
| `name` | Display label for the input |
| `value` | Current/default value |
| `validation` | Validation rules |
| `UI` | UI-specific options (semicolon-separated) |
| `details` | Tooltip text (from cell comment) |

### Input Types (UI Options)

#### Text & Numeric Inputs
| UI Type | Description | Example |
|---------|-------------|---------|
| (default) | Standard text/number field | `UI: ""` |
| `textarea` | Multi-line text input | `UI: textarea` |
| `nobuttons` | Numeric without +/- buttons | `UI: nobuttons` |

#### Sliders & Ranges
| UI Type | Description | Example |
|---------|-------------|---------|
| `slider` | Single-handle slider | `UI: slider;min=0;max=100` |
| `range` | Dual-handle range slider | `UI: range;min=0;max=100` |
| `delta=N` | Increment step between slider ticks | `UI: slider;min=0;max=100;delta=10` |
| `gridNum=N` | Number of ticks on slider grid | `UI: slider;min=0;max=100;gridNum=5` |

#### Dropdowns & Selection
| UI Type | Description | Example |
|---------|-------------|---------|
| `dropdown` | Dropdown from data validation list | `UI: dropdown` |
| `select` | Same as dropdown | `UI: select` |
| `multiple` | Multi-select dropdown | `UI: multiple` |
| `recorddropdown` | Dropdown populated from the app's record table | `UI: recorddropdown;display=customerName` |
| `rowdropdown` | Dropdown populated from any database table | `UI: rowdropdown;tableName=data_my-app_0;display=name;map=name->custName` |

To create a dropdown: create a named range with the list values, add Data Validation to the input cell referencing that range, and set `UI: dropdown`.

**Database dropdowns (`recorddropdown` and `rowdropdown`)** load options from a database table via AJAX. See `database.md` for full configuration.

#### Date & Time
| UI Type | Description | Example |
|---------|-------------|---------|
| `date` | Date picker | `UI: date` |
| `time` | Time picker | `UI: time` |

#### Special Inputs
| UI Type | Description | Example |
|---------|-------------|---------|
| `fileupload` | File upload | `UI: fileupload;maxFiles=3;acceptedFileTypes=.pdf,.jpg` |
| `signature` | Signature capture | `UI: signature` |
| `barcode` | Barcode/QR scanner | `UI: barcode` |
| `infotext` | Read-only info display (can contain HTML) | `UI: infotext` |
| `longinfotext` | Long read-only info (can contain HTML) | `UI: longinfotext` |

**Boolean toggle buttons:** Inputs with boolean values (`True` or `False`) automatically render as clickable toggle buttons instead of text fields.

### Numeric Display Options
| Option | Description | Example |
|--------|-------------|---------|
| `min=N` | Minimum allowed value | `UI: min=0` |
| `max=N` | Maximum allowed value | `UI: max=100` |
| `delta=N` | Increment step for +/- buttons and slider ticks | `UI: delta=0.5` |
| `multiplier=N` | Display multiplier | `UI: multiplier=100` (shows 0.5 as 50) |
| `prefix=X` | Display prefix (inputs only) | `UI: prefix=$` |
| `postfix=X` | Display suffix (inputs only) | `UI: postfix=%` |
| `decimals=N` | Number of decimal places | `UI: decimals=2` |
| `placeholder=X` | Placeholder text | `UI: placeholder=Enter value...` |
| `regEx=X` | Regex pattern for validation | `UI: regEx=^[A-Z]{2}[0-9]{4}$` |

### Layout & Display Options
| Option | Description | Example |
|--------|-------------|---------|
| `hidden` | Hide the input | `UI: hidden` |
| `noTitle` | Hide the label | `UI: noTitle` |
| `class=X` | Custom CSS class - applied to `.panel-body`, not the outer `.panel`. For inputs, Molnify appends `-form-group-class` (e.g., `class=half-left` → `.half-left-form-group-class`) | `UI: class=my-custom-class` |
| `dividerName=X` | Add section divider above | `UI: dividerName=Personal Details` |
| `tab=X` | Start a new tab - all following inputs are placed in this tab until the next `tab=`. Only set on the **first** input of each tab, not on every input. | `UI: tab=Advanced` |

### Conditional Display
Show/hide inputs and outputs based on variable values:
| Option | Description | Example |
|--------|-------------|---------|
| `showIfVariable=X` | Variable name to check | `UI: showIfVariable=myVar` |
| `showIfValue=X` | Show if variable equals value | `UI: showIfVariable=myVar;showIfValue=Yes` |
| `showIfValueNot=X` | Show if variable NOT equals value | `UI: showIfVariable=myVar;showIfValueNot=No` |
| `resetWhenHidden` | Reset value to default while hidden (prevents submission of stale data). Previously entered values reappear when the input is shown again. | `UI: showIfVariable=myVar;showIfValue=Yes;resetWhenHidden` |

To use conditional display, first expose the controlling input or output as a variable with `variable=X`, then reference it in dependent inputs.

### User Context Variables
Automatically populate inputs with data from the user's session. These are set in the input's **UI cell** and are populated once when the app loads (before the first calculation). Not related to database autofill (`autofill.*` metadata).

| Variable | Value | Notes |
|----------|-------|-------|
| `user.email` | Logged-in user's email | Preferred over `user` |
| `user` | User's email (or identification number if available) | Legacy alias - use `user.email` for clarity |
| `user.ip` | User's IP address | |
| `superuser` | `"true"` or `"false"` | String, not boolean - compare with `="true"` in formulas |
| `manager` | `"true"` or `"false"` | String, not boolean - compare with `="true"` in formulas |
| `currentRealm` | Current realm name | Only meaningful when `Realms` metadata is set |
| `tag1` through `tag10` | Custom per-user data fields | **Requires `DBUsers: TRUE`** - these fields are managed via the sidebar user interface |

Example: `UI: user.email` auto-fills with the user's email.
Example: `UI: user.email;hidden` auto-fills and hides the input (common for tracking who submitted).

### JavaScript Integration
| Option | Description | Example |
|--------|-------------|---------|
| `variable=X` | Expose value to JavaScript (ASCII only - no ö, ä, å) | `UI: variable=myInput` |
| `jsOnChange=X` | Call JS function on change | `UI: jsOnChange=handleInputChange` |

---

## Outputs Reference

### Basic Properties
Same as inputs: `cell`, `name`, `value`, `UI`, `details`

### Output Display Options
| Option | Description | Example |
|--------|-------------|---------|
| `icon=X` | Font Awesome 4.7.0 icon (FA5/6 names won't render) | `UI: icon=fa-line-chart` |
| `background=X` | Background color | `UI: background=#f0f0f0` |
| `color=X` | Text color | `UI: color=#333333` |
| `amongInputs` | Show in input panel (html outputs only). **Tab placement depends on physical row order** - see note below. | `UI: amongInputs` |
| `hidden` | Hide the output | `UI: hidden` |
| `hideCopy` | Hide copy button (html outputs only) | `UI: hideCopy` |
| `leftColumn` | Force into left column (works on html, charts, tables, peopleMatrix) | `UI: leftColumn` |
| `rightColumn` | Force into right column (works on html, charts, tables, peopleMatrix). Without this, panels alternate between columns. **Required when using CSS Grid on `#boxRow`** - the default alternation logic doesn't apply in Grid layouts. | `UI: rightColumn` |
| `panelHidden` | Panel collapsed by default | `UI: panelHidden` |
| `dividerName=X` | Section divider above output (requires amongInputs) | `UI: dividerName=Revenue and profit` |
| `peopleMatrix` | People/team matrix visualization | `UI: peopleMatrix;columns=5;rows=3` |

**`amongInputs` and tabs:** Molnify assigns items to tabs based on physical row order in the spreadsheet. An `amongInputs` output with `tab=X` will only join an existing tab created by inputs if it appears **between those input rows** in the spreadsheet. If the output is placed after all inputs (e.g., because AppBuilder writes inputs first, then outputs), `tab=X` creates a **new, separate tab** instead of joining the existing one. To place an `amongInputs` output inside an input tab, it must be physically interleaved among the inputs in the spreadsheet. Use `app.add_output(..., among_inputs=True)` in AppBuilder to achieve this.

### Charts and Tables Structure

Charts and tables use **blue cells** for data values only.

**CRITICAL: Only the numeric/data value cells are colored blue. Everything else has NO color:**
- Chart title cell - NO color
- Column headers (series names) - NO color
- Row labels (category names) - NO color
- UI configuration cell - NO color

**Example - multi-series (single-series is the same with one data column; pie/donut use one column too):**
```
|     | A              | B          | C          | D          | E          |
|-----|----------------|------------|------------|------------|------------|
| 1   | Sales Trend    | Product A  | Product B  | Product C  | lineChart  |  <- NO color (all headers)
| 2   | Q1             | [BLUE]100  | [BLUE]150  | [BLUE]80   |            |  <- A2 no color, B2:D2 BLUE
| 3   | Q2             | [BLUE]120  | [BLUE]140  | [BLUE]95   |            |  <- A3 no color, B3:D3 BLUE
| 4   | Q3             | [BLUE]110  | [BLUE]160  | [BLUE]100  |            |  <- A4 no color, B4:D4 BLUE
```

**Key rules:**
- Data cell color: RGB 0,112,192 / hex #0070C0
- Rows where ALL values are empty (`""`) are hidden automatically
- Series (columns) where ALL values are empty are hidden automatically
- For pie/donut charts, series with all zeros are also hidden
- Column titles (row 1) become legend/series names
- Row labels (column A) become x-axis categories

### Chart Types

| Chart Type | Description | Example |
|------------|-------------|---------|
| `barChart` | Vertical bar chart | `UI: barChart` |
| `lineChart` | Line chart | `UI: lineChart` |
| `pieChart` | Pie chart (single column) | `UI: pieChart` |
| `donutChart` | Donut chart (single column) | `UI: donutChart` |
| `waterfallChart` | Waterfall chart | `UI: waterfallChart` |
| `scatterChart` | Scatter plot | `UI: scatterChart` |
| `geoChart` | Geographic map | `UI: geoChart;map=world_mill` |
| `lineBarChart` | Combined line/bar (first series = bars, rest = lines) | `UI: lineBarChart` |
| `table` | Data table | `UI: table` |

### Chart Options
| Option | Description | Example |
|--------|-------------|---------|
| `stacked` | Stacked bars (default off) | `UI: barChart;stacked` |
| `horizontal` | Horizontal bars | `UI: barChart;horizontal` |
| `showValues` | Show values on top of bars | `UI: barChart;showValues` |
| `xAxis=X` | X-axis title | `UI: lineChart;xAxis=Time` |
| `yAxis=X` | Y-axis title | `UI: lineChart;yAxis=Value` |
| `yAxisTicks=N` | Number of Y-axis ticks | `UI: lineChart;yAxisTicks=5` |
| `decimals=N` | Decimals for values displayed on chart | `UI: barChart;decimals=2` |
| `axisDecimals=N` | Decimals on axis labels | `UI: axisDecimals=1` |
| `noGridLines` | Hide grid lines | `UI: lineChart;noGridLines` |
| `steps` | Step interpolation for lines | `UI: lineChart;steps` |
| `atLeast=N` | Y-axis must include this value | `UI: barChart;atLeast=0` |
| `atLeastSync` | Sync Y-axis range for dual Y-axes (lineBarChart) | `UI: lineBarChart;atLeastSync` |
| `centerZero` | Center zero on Y-axis (negative values mirror positive) | `UI: barChart;centerZero` |
| `hideMaxMin` | Hide max/min labels on Y-axis | `UI: lineChart;hideMaxMin` |
| `staggerLabels` | Stagger X-axis labels (auto-enabled if >3 values) | `UI: barChart;staggerLabels` |
| `noWordWrap` | Disable label word wrap (horizontal bars only) | `UI: barChart;horizontal;noWordWrap` |
| `noControls` | Hide legend and stacked/grouped toggle | `UI: barChart;noControls` |
| `noGroupedStackedControls` | Hide only stacked/grouped toggle | `UI: barChart;noGroupedStackedControls` |
| `map=X` | Map name for geoChart | `UI: geoChart;map=world_mill` |
| `class=X` | Custom CSS class - applied to `.panel-body`, not the outer `.panel`. Use `$('.my-class').closest('.panel')` in JS to target the outer panel. | `UI: barChart;class=revenue-chart` |

**Scatter chart specific:**
- Column headers must use these reserved names: `x`, `y`, and optionally `series`, `size`, `shape`
- `atLeast` takes four comma-separated values: `xMin,xMax,yMin,yMax`

**Waterfall chart specific:** Use value `e` in a data cell to mark it as a subtotal row (sums all values above)

### Chart Series Styling
Style individual series:
```
UI: lineChart;series1.color=#FF0000;series2.color=#00FF00;series1.strokewidth=3;series2.dashedline;series1.showarea
```

| Property | Description |
|----------|-------------|
| `seriesN.color=X` | Series color (hex or name) |
| `seriesN.strokewidth=N` | Line thickness |
| `seriesN.dashedline` | Dashed line style |
| `seriesN.showarea` | Fill area under line |

### HTML Panels
Display custom HTML:
```
UI: html
```
The cell value should contain HTML content.

---

## Actions Reference

Actions are automated operations triggered by button clicks. Each action is defined in yellow cells in the spreadsheet. Action property values are regular cell values - they can be formulas that reference inputs/outputs, and are evaluated during calculation before the action runs.

### Common Action Properties
| Property | Description |
|----------|-------------|
| `type` | Action type (required) |
| `title` | Button label |
| `name` | Internal name (used by `performActionWithName()`) |
| `position` | Button position |
| `hidden` | TRUE to hide button |
| `cssClass` | Custom CSS class |
| `showIfCell` | Conditional display cell |
| `showIfValue` | Show if cell equals value |
| `showIfValueNot` | Show if cell NOT equals value |
| `skip` | TRUE to skip execution (returns success without running) |
| `skipMessage` | Message to return when action is skipped |

### Success Handling Properties
These properties control what happens in the UI after an action completes successfully:
| Property | Description |
|----------|-------------|
| `successText` | Message displayed in the action modal |
| `successUrl` | Redirects the entire page (including parent if in iframe) to this URL |
| `successFrameUrl` | Redirects only the current frame to this URL (stays within iframe) |
| `successNewTabURL` | Shows an "Open in new tab" button in the modal that opens this URL |
| `successCalculate` | TRUE to trigger app recalculation after the action |
| `successRefreshScenarioList` | TRUE to reload the scenario dropdown list |
| `successHTMLElementId` | DOM element ID to update (used with successHTMLElementContent) |
| `successHTMLElementContent` | HTML injected into the element; modal is hidden automatically. Can include `<script>` tags for JS execution |
| `successSilent` | TRUE to hide modal immediately with no message |
| `successJavaScript` | JavaScript code to execute after the action succeeds (runs via `eval`) |

If none of these are set, a default success message is shown and the modal stays open until the user closes it.

**Using `successHTMLElementId`:** This property injects HTML into an existing DOM element after the action succeeds. The target element must already exist in the page. To create a target element, add an `infotext` input with the desired `id` as an HTML attribute:
```
# Create target element using an infotext input
app.add_input("", '<div id="actionResult"></div>', ui="infotext;noTitle;hidden")

# Reference it in the action
app.add_action({
    "type": "addrecord",
    "successHTMLElementId": "actionResult",
    "successHTMLElementContent": '<div class="alert alert-success">Saved!</div>',
})
```
**Do NOT use existing page element IDs** like `content`, `app`, `boxRow`, or `inputpanel` - injecting HTML into these will destroy the page layout. Always create a dedicated target element.

### Action Types

#### Email Action (`type: email`)
Send emails via Mailjet.
| Property | Description | Default |
|----------|-------------|---------|
| `to` | Recipient email(s), comma or semicolon separated | (required) |
| `from` | Sender display name | "Molnify sender" |
| `email` | Sender email address | "no-reply@molnify.com" |
| `subject` | Email subject | "Email from an app on Molnify!" |
| `content` | Plain text body | - |
| `contentHTML` | HTML body (overrides `content` if both set) | - |

Either `content` or `contentHTML` should be provided. If neither is set, a default placeholder message is sent. Default success message: "Email sent!"

**No template substitution:** The `content` and `contentHTML` values are sent as-is. There is no `{{variable}}` substitution for email actions. To include dynamic values, make the action property a **formula** that concatenates HTML with cell references:
```
contentHTML: ="<h1>Your BMI: "&App!B4&"</h1><p>Category: "&App!B5&"</p>"
```

**Limitations:** No CC/BCC, no attachments, no reply-to address. Recipients are split on `,` and `;` without trimming whitespace - use `"a@x.com,b@x.com"` not `"a@x.com, b@x.com"`.

#### HTTP Action (`type: http`)
Make HTTP requests to external APIs.
| Property | Description | Default |
|----------|-------------|---------|
| `url` | Endpoint URL | (required) |
| `method` | HTTP method (GET, POST, PUT, etc.) | "POST" |
| `payload` | Request body (overridden if `autoPayload` produces data) | "OK" |
| `autoPayload` | Auto-generate JSON payload: `inputs`, `outputs`, or `all` | - |
| `token` | Authorization token (added to Authorization header) | - |
| `authorizationType` | Auth scheme prefix for token | "Token" |
| `contentType` | Content-Type header | "application/json" |
| `responseVariable` | Variable to store the response body in | "httpresponse" |

**Response handling:** Only HTTP 200 and 201 are treated as success - other 2xx codes (202, 204, etc.) are treated as failures. The response body is set on the input matching `responseVariable` (default `httpresponse`), allowing you to parse it with `FILTERJSON()`. Newlines are stripped from the response body.

**`autoPayload` details:** Generates a JSON object using **cell references** as keys (e.g. `{"Sheet1!B2": "value"}`), not variable names. `inputs` includes all input cells, `outputs` includes all output cells, `all` includes both. If the generated JSON is empty, the original `payload` value is used instead.

**Request details:**
- `Accept` header is always `application/json` (not configurable)
- `User-Agent` is set to `Molnify-HTTP-Connector/1.1`
- Connect timeout: 5 seconds, read timeout: 12 seconds
- No automatic redirect following (3xx responses are treated as failures)

Default success message: "All received. Thank you!"

#### Scenario Save Action (`type: scenariosave`)
Save current input values as a named scenario.
| Property | Description | Default |
|----------|-------------|---------|
| `newName` | Scenario name | (required) |
| `oldName` | Previous name to delete (for rename operations) | - |
| `private` | TRUE for private scenario, FALSE for public | "true" |
| `allowOverwrite` | TRUE to allow others to overwrite this scenario | "false" |
| `overwriteCurrentScenario` | TRUE to update the currently loaded scenario | "false" |

Use with `successRefreshScenarioList: TRUE` to update the scenario dropdown after saving.

#### Generate Report Action (`type: generatereport`)
Generate PDF reports from templates. Runs asynchronously. The template syntax depends on the `engine` - HTML/markdown use Mustache, DOCX uses poi-tl, XLSX uses named ranges. See `report-templates.md` for details.

| Property | Description | Default |
|----------|-------------|---------|
| `template` | Template name (created via Template Manager) | "template" |
| `engine` | Conversion engine: `html`, `markdown`, `docx`, `xlsx` | "html" |
| `fileName` | Output PDF filename (.pdf added automatically) | "{appId}-molnify.pdf" |
| `suppressDownload` | TRUE to not auto-download the file | "false" |
| `to` | Email recipient (sends email with report link) | - |
| `content` | Plain text email body (use `{{urlToReport}}` placeholder) | - |
| `contentHTML` | HTML email body (use `{{urlToReport}}` placeholder) | - |
| `saveIp` | FALSE to not save user IP in metadata | "true" |

**PDF rendering properties:**

| Property | Description | Default |
|----------|-------------|---------|
| `landscape` | TRUE for landscape orientation | "false" |
| `paperSize` | Paper size: LETTER, LEGAL, TABLOID, LEDGER, A0-A6 | "LETTER" |
| `cssPageSize` | TRUE to use CSS `@page` size instead of `paperSize` | "false" |
| `margin` | Default margin for all sides (inches) | "0.5" |
| `marginTop`, `marginBottom`, `marginLeft`, `marginRight` | Override individual margins | uses `margin` |
| `waitDelay` | Milliseconds to wait before PDF conversion (for JS rendering) | "0" |
| `waitForExpression` | JavaScript expression that must return truthy before conversion | - |
| `pageRanges` | Page ranges to include (e.g., "1-3,5") | all pages |
| `includeBackground` | TRUE to print background graphics/colors | "false" |
| `mediaType` | CSS media type: `screen` or `print` | "print" |
| `performanceMode` | TRUE to skip rendering delay | "false" |

Shows "Preparing your file" in the modal while generating. Once ready, the file downloads automatically. Use `suppressDownload: TRUE` to skip the download while still emailing the report link via the `to`/`content`/`contentHTML` options. If no `content`/`contentHTML` is set but `to` is provided, the email body is just the report URL.

#### Generate File Action (`type: generatefile`)
Generate Excel or PDF files from XLSX templates. Runs asynchronously. **Prefer `generatereport`** for most use cases - `generatefile` is only needed when you want the output as an `.xlsx` file (not PDF) or need workbook password protection.
| Property | Description | Default |
|----------|-------------|---------|
| `template` | XLSX template filename | (required) |
| `format` | Output format: `pdf` or `xlsx` | "pdf" |
| `fileName` | Output filename | "{appId}-molnify.{format}" |
| `landscape` | TRUE for landscape orientation (PDF only) | "false" |
| `overwriteFormatting` | TRUE to apply template formatting to output | "true" |
| `protectWorkbook` | TRUE to password-protect the Excel output | "false" |
| `workbookPassword` | Password for workbook protection | - |

Shows "Preparing your file" in the modal while generating. Once ready, the file downloads automatically.

#### Database Actions

**Insert/Update Record (`type: addrecord`)**
Insert or update a record in the application's configured record table.
| Property | Description | Default |
|----------|-------------|---------|
| `saveIp` | TRUE to save user IP address | "true" |
| `recordTableName` | Override the app's default record table for this action | app's `RecordTableName` metadata |
| `idVariable` | Variable that holds/receives the record ID | app's `RecordTableIdVariable` metadata, or `"recordId"` |

Requires `RecordTableName` metadata (or `recordTableName` action option) to be set to the full table name including the `data_` prefix (e.g., `data_my-app_0`). Maps input/output variables to database columns. If the ID variable has a value, updates that record; otherwise creates a new record. The new/updated record ID is automatically set on the input matching `idVariable`.

**Important:** Only modified ("dirty") inputs are sent. Columns for unmodified inputs need `DEFAULT` values or must be nullable. See `database.md` for details.

**Insert SQL Row (`type: insertrow`)**
Insert or update a row in a specific database table with explicit column mapping.
| Property | Description | Default |
|----------|-------------|---------|
| `tablename` | Target table name (include `data_` prefix for consistency) | (required) |
| `.{columnname}` | Column value (prefix column name with `.`) | - |
| `k.{columnname}` | Primary key column (prefix with `k.` for upsert) | - |
| `idVariable` | Variable that receives the inserted/updated row ID | - |
| `saveIp` | TRUE to save user IP address | "true" |

Uses INSERT ... ON DUPLICATE KEY UPDATE for upsert behavior. Automatically adds `_molnify_user_email`, `_molnify_application_id`, `_molnify_user_ip` columns.

**Important:** Only modified ("dirty") inputs are sent. Columns for unmodified inputs need `DEFAULT` values or must be nullable. See `database.md` for details.

#### Download Query Action (`type: downloadquery`)
Download SQL query results as a file.
| Property | Description | Default |
|----------|-------------|---------|
| `query` | SQL SELECT query to execute | (required) |
| `format` | Output format: `csv` or `json` | "csv" |

Shows "Preparing your file" in the modal, then downloads automatically. CSV files use semicolon delimiter and include UTF-8 BOM. Filename format: `YYYYMMDD-HHmm-{appId}.{format}`.

#### E-Sign Action (`type: createesign`)
Create e-signature request via Zigned. Runs asynchronously.
| Property | Description | Default |
|----------|-------------|---------|
| `template` | Template name (created via Template Manager) | (required) |
| `provider` | E-sign provider (only "zigned" supported) | (required) |
| `fileName` | Output PDF filename | "{appId}-molnify.pdf" |
| `attachments` | Comma-separated URLs of files to attach | - |
| `saveIp` | TRUE to save user IP in metadata | "true" |

#### Reset Inputs Action (`type: resetinputs`)
Reset input variables to specified values or their spreadsheet defaults.
| Property | Description | Default |
|----------|-------------|---------|
| `.{variableName}` | Value to reset to (prefix variable name with `.`). Empty = use spreadsheet default. | - |

If no `.{variableName}` properties are specified, all inputs that have a `variable` are reset to their spreadsheet defaults. Otherwise, only the listed variables are reset.

Example:
```
type: resetinputs
title: Reset Form
.name: Default Name
.age:
```
Resets `name` to `"Default Name"` and `age` to its spreadsheet default value. Inputs without a `variable` UI option are not affected.

#### Multiple Action (`type: multiple`)
Execute several actions in sequence with a single button click.
| Property | Description |
|----------|-------------|
| `1`, `2`, `3`, ... | Name of the sub-action at each step (references another action's `name` property) |

Number sub-actions starting from 1. Each references another action by its `name` property. Sub-actions run in numeric order and **execution stops immediately if any sub-action fails** - later sub-actions do not run.

Success handling properties (`successText`, `successUrl`, etc.) set on the multiple action take precedence over those from sub-actions.

**Do not wrap `aiprompt` or `http` actions inside `multiple` if you need their response.** The `multiple` wrapper consumes the `responsevalue` field - the AI or HTTP response becomes inaccessible. Use `skip` formulas on the action directly for conditional execution, and chain follow-up work via `successJavaScript` instead.

Example - define a `scenariosave` action named `saveStep` and an `email` action named `emailStep`, both with `hidden: TRUE`. Then create the visible button:
```
type: multiple
title: Save & Email
1: saveStep
2: emailStep
successText: Saved and emailed!
```

#### AI Prompt Action (`type: aiprompt`)
Send a prompt to an AI model (Google Gemini) and store the response in a variable.

**Prerequisites:** Before using this action, the app uploader must: (1) be granted access to the AI model, (2) have created a token pool, and (3) have assigned the app to the token pool. All of this is managed in the AI Manager.

| Property | Description | Default |
|----------|-------------|---------|
| `model` | Model identifier: `gemini-flash` or `gemini-pro` | (required) |
| `prompt` | The prompt text to send to the AI model | (required) |
| `maxOutputTokens` | Maximum tokens in the response | 1024 |
| `thinkingBudget` | Token budget for model reasoning (must be > 0 for `gemini-pro`) | 0 |
| `responseSchema` | JSON schema to enforce structured response format | - |
| `responseVariable` | Variable name to store the AI response in | "aipromptresponse" |

**Models:**

| Model | Best For |
|-------|----------|
| `gemini-flash` | Fast responses, summarization, extraction, real-time use |
| `gemini-pro` | Complex analysis, long-form generation, planning, reasoning-heavy tasks |

`gemini-pro` requires `thinkingBudget` > 0 and consumes more tokens per request than `gemini-flash`.

**Response handling:** The AI response text is set on the input matching `responseVariable` (default `aipromptresponse`), allowing you to display it or parse it with `FILTERJSON()`. If `responseSchema` is provided, the response is constrained to valid JSON matching the schema - use `FILTERJSON()` on the response variable's cell to extract individual fields from it.

**Token quota:** Each execution consumes tokens from the app's assigned token pool. If the pool is exhausted, the action returns an error.

**Dynamic prompts:** Like all action properties, `prompt` can be a formula that references inputs/outputs:
```
prompt: ="Summarize the following text in "&B2&" words: "&B3
```

**Structured output with `responseSchema`:** Provide a JSON schema to get predictable, parseable responses. Use `FILTERJSON()` on the response variable's cell to extract fields from the JSON response:
```
type: aiprompt
model: gemini-flash
prompt: ="Classify this feedback as positive, negative, or neutral: "&B2
responseSchema: {"type":"object","properties":{"sentiment":{"type":"string","enum":["positive","negative","neutral"]},"confidence":{"type":"number"}},"required":["sentiment","confidence"]}
responseVariable: classification
maxOutputTokens: 256
```
If `classification` is in cell B10, use `=FILTERJSON(B10, "sentiment")` and `=FILTERJSON(B10, "confidence")` in output cells to extract the values.

**Example - simple text generation:**
```
type: aiprompt
title: Generate Summary
model: gemini-flash
prompt: ="Write a 3-sentence summary of: "&B2
responseVariable: summary
successSilent: TRUE
successCalculate: TRUE
```

---

## Metadata Reference

Metadata cells (purple) configure application-wide settings.

### Application Identity
| Property | Description |
|----------|-------------|
| `Name` | Application name (shown in header and page title) |
| `Author` | Author name (shown in app info modal) |
| `Website` | Website URL |
| `Version` | Version string |
| `Description` | App description (shown in app info modal) |
| `Reference` | Reference/documentation URL (shows reference button if set) |

### Panel Customization
| Property | Default | Description |
|----------|---------|-------------|
| `InputPanelTitle` | "Inputs" | Input section title |
| `OutputPanelTitle` | "Results" | Output section title |
| `CalculateTitle` | "Calculate" | Calculate button text |
| `CopyTitle` | - | Copy button text |
| `SaveTitle` | - | Save button text |

### Feature Toggles
| Property | Default | Description |
|----------|---------|-------------|
| `AutoCalcEnabled` | TRUE | Auto-calculate on input change. When FALSE, no calculation runs automatically - not on load, not on input change; you must trigger calculation yourself (e.g. `calculateButton()` in `JavaScriptAfterLoad` for the initial calc). **Avoid disabling unless truly required** - disabling as a performance optimization breaks the expected UX and introduces subtle bugs. |
| `EnabledForCalculate` | TRUE | Show calculate button (only visible when AutoCalc is FALSE) |
| `EnabledForSave` | FALSE | Enable scenario saving (shows save/delete/share buttons) |
| `EnabledForReset` | FALSE | Show reset button |
| `EnabledForPrint` | TRUE | Show print button |
| `EnabledForUpdate` | FALSE | Show update button (allows anyone to re-upload/update the app) |
| `EnabledForLogOut` | FALSE | Show logout button (only visible when user is signed in) |
| `EnabledForDynamicTitles` | TRUE | Recalculate input/output titles on each calculation |
| `CookieConsentPopup` | FALSE | Show cookie consent dialog on app load |
| `Headless` | FALSE | Serve a minimal page instead of the full UI. Provides `window.molnify` bootstrap object, `MolnifySDK`, and empty `<div id="app">`. Only `JavaScript` metadata runs (not `JavaScriptAfterLoad`/`JavaScriptAfterCalc`). See `custom-frontend.md`. |

### User Management
| Property | Description |
|----------|-------------|
| `Users` | Comma-separated emails or domains (`*@company.com`) to restrict access. If not set, app is public. |
| `SuperUsers` | Comma-separated emails with elevated permissions (can manage users and managers) |
| `Managers` | Comma-separated emails who can manage regular users and view all scenarios |
| `Realms` | Comma-separated realm names (e.g., "Europe,Asia,Americas") - creates a realm selector dropdown |
| `DBUsers` | TRUE to manage users via the sidebar interface instead of metadata |

**Note:** `Users`, `SuperUsers`, and `Managers` can also reference a cell range (e.g., `users!A2:A200`) instead of a comma-separated list.

### Styling
| Property | Description |
|----------|-------------|
| `CSS` | Complete custom CSS |
| `additionalCSS` | CSS appended after the main `CSS` value. Use when a template provides base CSS and you want to extend it without replacing it. Also useful when CSS exceeds a single cell's 32,767-character limit - split across `CSS` and `additionalCSS`. |
| `TopBannerColor` | Top banner color. Paints the **top banner** (`#header`, the fixed full-width bar above the app), via `#molnifyAppBody .container-fluid`. This is a different element from `#appHeaderRow` (the app title bar inside `#content`) - it does not color that. |
| `HeaderTextColor` | Colors the **app title** at the top of the content area (`h1#appHeader`/`#pAppTitle`, via `#molnifyAppBody h1`) - not the top banner (a logo, no text). The title sits on the page background, light by default, so keep it **dark**; only go light for a dark-background app. |
| `PanelHeaderColor` | Panel header color |
| `ButtonColor` | Default button color |
| `ButtonActiveColor` | Selected button color |
| `SuccessButtonColor` | Success/action button color |
| `ExpandButtonColor` | Expand button color |
| `CollapseButtonColor` | Collapse button color |
| `OutputBoxBackgroundColor` | Output box background |
| `ChartDownloadBackgroundColor` | Background color for downloaded chart images |
| `ToggleActiveColor` | Active state color for boolean toggle buttons |
| `BackgroundImageURL` | Background image URL for the app |
| `LogoURL` | Custom logo URL (replaces "Molnify" text in top banner) |
| `FaviconURL` | Custom favicon URL (browser tab icon, should be .ico or small .png) |
| `AppleTouchIconURL` | Apple touch icon URL (for iOS home screen, typically 180x180 .png) |
| `HeaderFont` | Header font family (Google Fonts name, e.g., "Roboto") |
| `BodyFont` | Body font family (Google Fonts name) |

### Slider Styling
| Property | Description |
|----------|-------------|
| `SliderMinAndMaxBackgroundColor` | Min/max label background |
| `SliderMinAndMaxTextColor` | Min/max label text color |
| `SliderCurrentValueBackgroundColor` | Current value background |
| `SliderCurrentValueTextColor` | Current value text color |

### Scenarios Configuration
| Property | Default | Description |
|----------|---------|-------------|
| `ScenarioTerm` | "Scenario" | Custom term (e.g., "Order", "Quote", "Case") - affects all UI labels |
| `RealmTerm` | "Realm" | Custom term for realm (e.g., "Region", "Department") |
| `ScenarioNameVariable` | - | Variable whose value becomes the default scenario name in save dialog |
| `ScenarioSavePrivate` | FALSE | TRUE to save scenarios as private by default |
| `ScenarioLockOption` | FALSE | TRUE to show lock icon for scenarios where overwrite is disabled |
| `ScenarioSharedUsersCell` | - | Cell containing comma-separated emails to share scenarios with |
| `RecordTableName` | - | Database table name for record storage (used with `addrecord` action) |
| `RecordTableIdVariable` | "recordId" | Column name used as the record ID in the record table. Overrideable per-action with `idVariable`. |
| `RecordTableAccessColumn` | - | Column name for row-level access control |
| `DataTable.N` | - | Declarative DB table schema (JSON). N=0-31 maps to `data_<appId>_N`. See [Data Persistence](#data-persistence). |

### JavaScript Integration
| Property | When it runs | Description |
|----------|--------------|-------------|
| `JavaScript` | Page load (early) | Custom JS code, runs before app is fully interactive |
| `additionalJavaScript` | Page load (early) | JavaScript appended after the main `JavaScript` value. Use when a template provides base JS and you want to extend it, or when JS exceeds a single cell's 32,767-character limit - split across `JavaScript` and `additionalJavaScript`. |
| `JavaScriptAfterLoad` | Page load (late) | JS to run after app fully loads and initial calculation completes |
| `JavaScriptAfterCalc` | Every calculation | JS to run after each calculation (including initial load) |

#### Available JavaScript Functions
| Function | Description |
|----------|-------------|
| `setValueForVariable(name, value)` | Set a variable's value (**triggers auto-calc if `AutoCalcEnabled: TRUE`** - see warning below) |
| `getValueForVariable(name)` | Get a variable's current value |
| `performActionWithName(name)` | Trigger an action by its name |
| `calculateButton()` | Trigger app recalculation |
| `getValueForURLParam(name)` | Get a URL query parameter value |

**WARNING - `setValueForVariable` and auto-calc loops:**
When `AutoCalcEnabled: TRUE` (the default), calling `setValueForVariable()` triggers a recalculation. If you call it inside `JavaScriptAfterCalc`, this creates an infinite loop: calc → afterCalc → setValue → calc → afterCalc → ... Always guard with a value comparison:
```javascript
JavaScriptAfterCalc:
var current = getValueForVariable('myVar');
var desired = computeNewValue();
if (current !== desired) setValueForVariable('myVar', desired);
```

Example - Auto-run action on load:
```javascript
JavaScriptAfterLoad: performActionWithName('loadData');
```

Example - Store parsed value:
```javascript
function saveToken() {
  let tok = getValueForVariable('token');
  if (tok !== '\u00A0') setValueForVariable('storedToken', tok);
}
```

### API & Security
| Property | Description |
|----------|-------------|
| `IpRanges` | Comma-separated IP ranges to restrict access (e.g., "192.168.1.0/24,10.0.0.0/8") |
| `TokenAuthentication` | Token authentication config |

### Custom Tooltips
| Property | Description |
|----------|-------------|
| `CustomInfoTooltip` | Info button tooltip |
| `CustomReferenceTooltip` | Reference button tooltip |
| `CustomUpdateTooltip` | Update button tooltip |
| `CustomSaveTooltip` | Save button tooltip |
| `CustomPrintTooltip` | Print button tooltip |
| `CustomResetTooltip` | Reset button tooltip |
| `CustomLogoutTooltip` | Logout button tooltip |
| `CustomDownloadDataTooltip` | Download button tooltip |
| `CustomDeleteTooltip` | Delete button tooltip |

### Advanced Options
| Property | Default | Description |
|----------|---------|-------------|
| `TopBannerHidden` | FALSE | TRUE to hide the top navigation banner |
| `HeaderHidden` | FALSE | TRUE to hide the app header row |
| `InputPanelSmall` | FALSE | TRUE for narrower input panel (33% width instead of 50%) |
| `InputPanelFixed` | FALSE | TRUE to keep input panel fixed while scrolling |
| `PanelsFixed` | FALSE | TRUE to keep both panels fixed while scrolling |
| `OutputBoxPanelHidden` | FALSE | TRUE to collapse output panel by default |
| `OnlyAppNameInPageTitle` | FALSE | TRUE for page title "AppName" instead of "AppName - Molnify" |
| `ClearClipboardAtReset` | FALSE | TRUE to clear copied values when reset button clicked |
| `ClearClipboardAtCalc` | FALSE | TRUE to clear copied values on each calculation |
| `CustomLogOutURL` | - | URL to redirect to after user logs out |
| `HeadHTML` | - | Raw HTML injected into `<head>` - use for CDN `<script>` or `<link>` tags |
| `Template` | - | Apply a built-in template |
| `OnlyIncludeSheet` | - | Only parse the named sheet (ignores all other sheets) |
| `calcTimerSeconds` | - | Auto-recalculate every N seconds (minimum 0.5). Sets up `setInterval(calculateButton, N*1000)`. |

---

## Validation Rules

Validation is specified in the validation property of inputs.

### Numeric Validation
| Rule | Description |
|------|-------------|
| `>N` | Greater than N |
| `>=N` | Greater than or equal to N |
| `<N` | Less than N |
| `<=N` | Less than or equal to N |
| `=N` | Equals N |

### Text Validation
| Rule | Description |
|------|-------------|
| `notEmpty` | Cannot be empty |

### Custom Validation
Use `var=variableName` to store validation dropdown options in a JavaScript variable.


---

*This is v1.0.13 of the skill, published 2026-07-14. Installed copies are version-pinned; to update to the latest release, re-run `npx skills add https://app.molnify.com` (see `README.md`).*
