Home > Nanoflows
Version: 1.0 Date: February 2026
- What Is a Nanoflow?
- When to Use Nanoflows vs. Microflows
- Supported Activities
- Working with Data
- Offline Support
- JavaScript Actions
- UI Interactions
- Performance Considerations
- Error Handling
- Security Implications
- Common Patterns
- Debugging
A nanoflow is a client-side logic flow in Mendix. It looks like a microflow in Studio Pro -- you drag activities onto a canvas, connect them, set conditions -- but the similarity is visual. The execution model is fundamentally different.
When a microflow runs, the Mendix Runtime on the server receives the request, executes each step against the database, and sends results back. When a nanoflow runs, everything happens in the user's browser (or native mobile device). No HTTP request goes to the server unless you explicitly make one.
Here is what happens when a nanoflow fires:
- The user triggers an event -- a button click, an on-change handler, a page load event.
- The Mendix Client (the JavaScript runtime that powers every Mendix app in the browser) picks up the event and starts executing the nanoflow steps.
- Each activity runs locally. Object creation creates an in-memory JavaScript object. Attribute changes modify that object in local memory. Retrieve operations pull from the client's local object cache (not the database).
- If the nanoflow includes a "Call Microflow" activity, that is the only point where the browser sends a request to the server. The nanoflow pauses, waits for the response, and then continues.
- When the nanoflow finishes, the Mendix Client applies any UI changes -- page navigations, refreshes, validation messages.
The Mendix Client compiles nanoflows into JavaScript at deployment time. They ship as part of the client bundle. This means there is zero server round-trip overhead for purely local logic.
Nanoflows are the only logic mechanism that works when the device has no network connection. This is by design:
- In an offline-first app, the Mendix Client maintains a local SQLite database on the device.
- Nanoflows operate against this local database for retrieves, creates, changes, and deletes.
- Data synchronization with the server happens separately -- either automatically or when you explicitly trigger it.
- Microflows cannot run offline. Period. If you try to call a microflow from an offline page, the call will fail.
This makes nanoflows mandatory for any offline-capable application, but their usefulness extends well beyond that scenario.
| Aspect | Nanoflow | Microflow |
|---|---|---|
| Execution location | Client (browser / native device) | Server (Mendix Runtime) |
| Network required | No (except for explicit server calls) | Yes, always |
| Database access | Client cache or local offline DB | Full server database |
| Language compiled to | JavaScript | Java |
| Available in offline apps | Yes | No (cannot be called directly) |
| Entity access rules applied | At sync / server boundary | On every operation |
| Visible to end user | Yes (source is in the client bundle) | No (runs server-side) |
The question is not "which is better?" -- it is "where should this logic run?" Client-side and server-side execution have different trade-offs, and picking the wrong one creates problems that are painful to fix later.
Use the following table to guide your decision. If any row lands firmly in the microflow column, use a microflow -- even if other rows favor a nanoflow.
| Factor | Use a Nanoflow | Use a Microflow | Notes |
|---|---|---|---|
| Offline requirement | Must work offline | Online-only is acceptable | This is a hard constraint. If the page must work offline, you have no choice -- nanoflows only. |
| Latency sensitivity | User expects instant response (< 50ms) | A brief loading state is acceptable | Nanoflows skip the network round-trip entirely. Useful for real-time form validation, toggling UI states, or filtering already-loaded data. |
| Data volume | Working with a small set already on the client (< 1,000 objects) | Querying or aggregating large datasets (thousands+ rows) | Nanoflows operate on objects already in the client cache. If you need to search through 50,000 records, do it on the server. |
| Security sensitivity | No sensitive business rules or secrets involved | Contains authorization logic, pricing rules, API keys, or any secret | Nanoflow logic is visible in the browser. Anyone with dev tools open can read it. |
| Integration calls | No external service calls needed | Calls REST/SOAP/OData services, sends emails, writes files | Nanoflows cannot directly call external services. You must route through a microflow. |
| Database writes | Creating/changing objects that will sync later (offline) or be committed via a microflow call | Direct, immediate database commits required | Nanoflows cannot commit to the server database directly. They modify the client cache. |
| Computation complexity | Simple: conditionals, string formatting, basic math | Complex: loops over large lists, heavy aggregations, PDF generation | The browser's JavaScript engine is single-threaded. Heavy computation blocks the UI. |
| Transaction requirements | No transactional guarantees needed | Must be atomic (all-or-nothing) | Only the server provides database transactions. Nanoflows have no transaction concept. |
| Scenario | Recommendation | Why |
|---|---|---|
| Validate a form field on change | Nanoflow | Instant feedback, no server needed, simple logic |
| Save a new record to the database | Microflow | Needs database commit and server-side validation |
| Toggle a boolean to show/hide a UI section | Nanoflow | Pure UI state, instant response |
| Calculate a running total on a page | Nanoflow (if data is already loaded) | Avoids a round-trip for each keystroke |
| Enforce that only managers can approve requests | Microflow | Authorization logic must not run on the client |
| Apply a discount based on secret pricing tiers | Microflow | Business rules and pricing logic must stay server-side |
| Navigate to another page | Nanoflow | Client-side navigation is instant |
| Generate a PDF report | Microflow | Requires server-side libraries and file handling |
| Filter a list that is already displayed on the page | Nanoflow | Data is already in the client cache |
| Search across the full customer database | Microflow | Requires server-side database query |
| Sync offline data when connectivity returns | Nanoflow triggers the sync | The sync activity is available in nanoflows |
| Send an email notification | Microflow | Requires server-side SMTP integration |
| Log an audit trail entry | Microflow | Audit data should be committed server-side, tamper-proof |
In practice, many user interactions combine both. A common pattern:
- Nanoflow validates the form locally (instant feedback).
- Nanoflow calls a microflow to persist the data (server commit).
- Nanoflow shows a success message or navigates to the next page.
This gives the user instant validation feedback while ensuring data integrity on the server. The nanoflow orchestrates the flow; the microflow handles the parts that require the server.
Not every activity in Studio Pro is available in nanoflows. The general rule: if it requires server infrastructure (database transactions, file storage, external service connectors), it is microflow-only. If it manipulates objects in memory or controls the UI, it works in nanoflows.
| Activity | Nanoflow | Microflow | Notes |
|---|---|---|---|
| Create Object | Yes | Yes | In nanoflows, creates the object in the client's memory / local DB. |
| Change Object | Yes | Yes | Modifies attributes on an in-memory object. |
| Delete Object | Yes | Yes | In nanoflows, removes from client cache / local DB. Server delete requires a microflow. |
| Retrieve | Yes | Yes | Nanoflow retrieves come from the client cache or local offline database -- never the server DB directly. |
| Commit | No | Yes | Nanoflows cannot commit to the server database. Use a microflow call to persist data. |
| Rollback | Yes | Yes | Reverts an object in the client cache to its last-known committed state. |
| Activity | Nanoflow | Microflow | Notes |
|---|---|---|---|
| Aggregate List | Yes | Yes | Runs on the client-side list. Keep the list small. |
| Create List | Yes | Yes | |
| List Operation (union, intersect, etc.) | Yes | Yes | |
| Filter/Find | Yes | Yes | Uses XPath on the client cache (limited compared to server XPath). |
| Sort | Yes | Yes | |
| Head / Tail | Yes | Yes |
| Activity | Nanoflow | Microflow | Notes |
|---|---|---|---|
| Create Variable | Yes | Yes | |
| Change Variable | Yes | Yes | |
| Decision (exclusive split) | Yes | Yes | |
| Merge | Yes | Yes | |
| Loop | Yes | Yes | Be careful with loop size -- runs on the UI thread. |
| Break / Continue | Yes | Yes | |
| End Event | Yes | Yes |
| Activity | Nanoflow | Microflow | Notes |
|---|---|---|---|
| Call Nanoflow | Yes | No | Nanoflows can call other nanoflows (composition). Microflows cannot call nanoflows. |
| Call Microflow | Yes | Yes | From a nanoflow, this triggers a server round-trip. The nanoflow waits for the response. |
| Call JavaScript Action | Yes | No | Nanoflow-exclusive. This is how you extend nanoflows with custom JavaScript. |
| Call Java Action | No | Yes | Java actions run on the server only. |
| Call REST Service | No | Yes | Use a microflow, then call that microflow from the nanoflow. |
| Call Web Service | No | Yes | Same approach -- wrap in a microflow. |
| Call External Action (OData) | No | Yes |
| Activity | Nanoflow | Microflow | Notes |
|---|---|---|---|
| Show Page | Yes | Yes | In nanoflows, navigation is instant (no round-trip). |
| Close Page | Yes | Yes | |
| Show Home Page | Yes | Yes | |
| Show Message | Yes | Yes | Dialogs in nanoflows appear without server delay. |
| Show Validation Message | Yes | Yes | |
| Download File | No | Yes | Files are stored server-side. Download requires a server response. |
| Show Progress | Yes | No | Nanoflow-only. Displays a blocking progress indicator. |
| Hide Progress | Yes | No | Paired with Show Progress. |
| Synchronize | Yes | No | Nanoflow-only. Triggers offline data sync. |
| Sign Out | Yes | Yes |
| Activity | Nanoflow | Microflow | Notes |
|---|---|---|---|
| Call REST | No | Yes | External HTTP calls must go through the server. |
| Call SOAP | No | Yes | |
| Import/Export XML Mapping | No | Yes | |
| Send Email (Email Connector) | No | Yes |
| Activity | Nanoflow | Microflow | Notes |
|---|---|---|---|
| Log Message | Yes | Yes | In nanoflows, logs to the browser console (not the server log). |
| Cast Object | Yes | Yes | |
| Generate Document | No | Yes | Requires server-side document template engine. |
| Call Workflow | No | Yes | Workflow engine runs server-side. |
If you are ever unsure, ask yourself: "Does this need something that only exists on the server?" If yes, use a microflow or call a microflow from your nanoflow. If no, a nanoflow is likely the better choice for responsiveness.
Every Mendix app in the browser maintains a client-side object cache. When a page loads, the Mendix Client fetches the objects that page needs and stores them in memory. Nanoflows work exclusively against this cache (in online apps) or the local SQLite database (in offline apps).
Understanding this is critical. When a nanoflow retrieves objects, it is not querying the server database. It is looking at what the client already has.
When you use "Create Object" in a nanoflow:
- A new object is instantiated in client memory.
- It has no database ID yet (it has not been committed).
- You can set attributes, associate it with other objects in the cache, and display it on a page.
- The object exists only on the client until you explicitly send it to the server (via a microflow call or offline sync).
Practical example -- creating a new order line:
[Start]
--> Create Object: OrderLine
- Quantity = 1
- UnitPrice = $Product/Price
- OrderLine_Order = $CurrentOrder
- OrderLine_Product = $SelectedProduct
--> Change Object: $CurrentOrder
- TotalAmount = $CurrentOrder/TotalAmount + $NewOrderLine/UnitPrice
--> [End: $NewOrderLine]
This entire flow runs instantly. The user sees the new line item appear on the page with no loading spinner. The data has not been saved to the server yet.
Nanoflow retrieves come from the client cache or local database. There are two retrieve modes:
| Mode | What It Does | When to Use |
|---|---|---|
| By Association | Follows an association from a given object to its related object(s). | When you already have the parent/related object in context. Fast and reliable. |
| From Database | Queries the client cache (online) or local SQLite DB (offline) using XPath. | When you need to find objects that are not directly associated with your current context. |
Important limitations of nanoflow retrieves:
-
Online apps: "From Database" retrieves search the client cache, not the server database. If an object is not already loaded on the client, the nanoflow will not find it. This catches people off guard regularly.
-
Offline apps: "From Database" retrieves query the local SQLite database, which contains a defined subset of server data (controlled by your offline-first data configuration). The data may be stale if the user has not synced recently.
-
XPath constraints: Nanoflow XPath supports a subset of what server-side XPath supports. Complex constraints involving reverse associations or nested path expressions may not work. Test your constraints early.
-
No aggregate queries against the database: You can aggregate a list you have already retrieved, but you cannot issue a COUNT or SUM query directly against the client-side database in XPath.
When you need data that is not in the client cache, the pattern is:
- Nanoflow calls a microflow with any parameters the server needs (search term, filter criteria, page number).
- Microflow queries the database, applies security rules, and returns the result (an object, a list, or a primitive).
- Nanoflow receives the result and continues.
Structure:
[Start]
--> Show Progress: "Searching..."
--> Call Microflow: ACT_SearchCustomers
- Parameter: $SearchTerm
- Return: $CustomerList (List of Customer)
--> Hide Progress
--> Decision: $CustomerList is empty?
- True --> Show Message: "No results found."
- False --> Show Page: CustomerSearchResults (pass $CustomerList)
--> [End]
Each microflow call is one HTTP request. The nanoflow blocks (waits) until the response comes back. Design your microflows to return everything the nanoflow needs in a single call when possible.
Nanoflows can change object attributes freely:
- Change Object modifies attributes in the client cache immediately.
- The object is in a "dirty" state -- it has unsaved changes.
- If the user navigates away or the page closes, uncommitted changes are lost (unless you have explicitly handled this).
- To persist changes, call a microflow that commits the object, or (in offline apps) the changes are committed locally and synced later.
Object passing between nanoflows:
When one nanoflow calls another, objects are passed by reference. This means the called nanoflow operates on the same in-memory object. Changes made in the inner nanoflow are immediately visible in the outer nanoflow when it resumes.
| Pattern | Description | When to Use |
|---|---|---|
| Local create, then server commit | Create and populate objects in a nanoflow, then call a microflow to commit them all at once. | Standard form submission in online apps. |
| Server fetch, then local display | Call a microflow to retrieve data, then use the returned objects in local nanoflow logic (filtering, sorting, formatting). | Search results, complex queries that need server-side data access. |
| Offline create, then sync | Create objects in the local DB, continue working offline, sync when connected. | Field worker apps, inspections, data collection. |
| Optimistic local update | Change the object locally and update the UI first, then call a microflow in the background to persist it. If the server call fails, revert. | Like buttons, toggles, quantity adjustments. |
| Cache-first read | Retrieve from the client cache first. If the result is empty or stale, call a microflow to refresh. | Dashboards, reference data, frequently viewed lists. |
Mendix offline-first apps use a local SQLite database on the device (or IndexedDB in the browser for PWAs). The architecture works like this:
-
Initial sync: When the user first opens the app, the Mendix Client downloads a subset of the server data to the local database. Which entities and how much data is controlled by the offline-first configuration in Studio Pro.
-
Normal operation: The user interacts with the app entirely through nanoflows. All reads, creates, changes, and deletes happen against the local database. The app works identically whether the device is online or offline.
-
Synchronization: Periodically (or when triggered), the Mendix Client syncs changes bidirectionally. Local changes are uploaded to the server; server changes are downloaded to the device.
In an offline-first profile page, you cannot attach a microflow to a button. Studio Pro will not allow it. Every piece of logic on offline pages must be a nanoflow. The only way to execute server-side logic is through the "Call Microflow" activity inside a nanoflow, and that call will fail if the device is offline (which you must handle).
In Studio Pro, each entity that should be available offline needs configuration:
| Setting | Description | Impact |
|---|---|---|
| Download mode | All, Constrained, None, or Nothing (for new objects only) | Controls what data is synced to the device. "All" syncs every record; "Constrained" uses an XPath to limit the set. |
| Sync behavior | Upload changes, Download changes, or Both | Controls sync direction. |
| XPath constraint | Filters which objects sync to the device | Use this to limit data per user. For example, sync only records assigned to the current user. |
What triggers a sync:
| Trigger | How It Works |
|---|---|
| Automatic (startup sync) | Happens when the app starts if the device is online. Downloads data based on offline configuration. |
| Nanoflow Synchronize activity | You explicitly trigger a full or selective sync from a nanoflow. |
| Synchronize Unsynchronized Objects | Syncs only objects that have local changes. More targeted than a full sync. |
| Save button (default) | In some page templates, the save button triggers a microflow call, which implicitly syncs the changed object. |
Selective synchronization:
Starting with Mendix 10, you can perform selective sync -- synchronizing specific entities or even specific objects rather than the full dataset. This is important for apps with large offline datasets where a full sync takes too long.
[Start]
--> Synchronize
- Type: Unsynchronized Objects
- Entities: [Order, OrderLine]
--> Decision: Sync successful?
- True --> Show Message: "Data saved."
- False --> Show Message: "Sync failed. Changes saved locally."
--> [End]
Conflicts occur when both the device and the server have changed the same object since the last sync. Mendix handles this with a "last write wins" strategy by default, but you need to understand the nuances:
| Scenario | Default Behavior | How to Handle |
|---|---|---|
| Device changed, server unchanged | Device changes are applied to the server. | No conflict. Works as expected. |
| Server changed, device unchanged | Server changes are downloaded to the device. | No conflict. Works as expected. |
| Both changed the same object | Device changes are sent to the server. The server applies them, overwriting its own changes. | This is "device wins." If you need different behavior, implement a conflict resolution microflow. |
| Device changed, server deleted the object | The sync will attempt to update a non-existent object. This triggers an error. | Handle this in error handling. You may need to recreate the object or inform the user. |
| Device deleted, server changed the object | The device sends a delete request. The server deletes the object, losing its own changes. | If the server change was important, you need custom conflict detection. |
Implementing custom conflict resolution:
- Add a
LastModifiedDateattribute (DateTime) to your entity. - Set it in both the nanoflow (local change) and the server-side microflow (before-commit).
- In the synchronization commit microflow, compare the incoming
LastModifiedDatewith the server's current value. - If the server's version is newer, you have a conflict. Decide: merge, reject, or prompt the user.
| Concern | Guidance |
|---|---|
| Local DB size | SQLite on mobile devices has practical limits. Keep offline datasets under 50MB where possible. Test on the lowest-spec target device. |
| Number of entities | Each offline entity adds to sync time. Only sync what the user actually needs offline. |
| Image/file sync | Binary data (images, documents) can bloat the local DB. Consider syncing file metadata only and downloading files on demand when online. |
| Sync duration | Full syncs on large datasets can take minutes on slow connections. Use selective sync. Show progress to the user. |
| Stale data | Offline data can be hours or days old. Show the last sync time to users so they know how fresh their data is. |
JavaScript actions are the escape hatch. When nanoflows cannot do something natively -- access a browser API, call a third-party JavaScript library, manipulate the DOM, interact with device hardware -- you write a JavaScript action and call it from a nanoflow.
A JavaScript action is a .js file inside your Mendix project that follows a specific contract: it receives typed parameters and returns a typed result. Studio Pro generates the boilerplate; you fill in the implementation.
In Studio Pro:
- Right-click a module folder > Add other > JavaScript action.
- Name it descriptively (e.g.,
GetCurrentGPSLocation,CopyToClipboard,FormatCurrency). - Define input parameters -- each has a name and a type (String, Integer, Boolean, Decimal, DateTime, Object, List, Enumeration).
- Define the return type -- same type options, or Nothing.
- Studio Pro generates a
.jsfile. Open it and implement the logic.
// This file was generated by Mendix Studio Pro.
//
// WARNING: Only the following code will be retained when actions are regenerated:
// - the import list
// - the code between BEGIN USER CODE and END USER CODE
// - the code between BEGIN EXTRA CODE and END EXTRA CODE
// Other code you write will be lost the next time you deploy the project.
import { Big } from "big.js";
// BEGIN EXTRA CODE
// Put reusable helper functions here.
// END EXTRA CODE
/**
* Copies the given text to the user's clipboard.
* @param {string} textToCopy - The text to copy.
* @returns {Promise.<boolean>} - True if successful, false otherwise.
*/
export async function CopyToClipboard(textToCopy) {
// BEGIN USER CODE
try {
await navigator.clipboard.writeText(textToCopy);
return true;
} catch (error) {
console.error("Failed to copy to clipboard:", error);
return false;
}
// END USER CODE
}Key rules:
- Only write code between
BEGIN USER CODE/END USER CODEandBEGIN EXTRA CODE/END EXTRA CODE. Everything else gets overwritten on regeneration. - The function must return a
Promiseif it does anything asynchronous (which most browser APIs do). - Studio Pro passes Mendix objects as JavaScript objects with getter/setter methods. Use
myObject.get("AttributeName")andmyObject.set("AttributeName", value). - For Big (decimal) values, Mendix uses the
big.jslibrary. The import is auto-included.
When you pass a Mendix object as a parameter, you interact with it through the Mendix Client API:
export async function CalculateLineTotal(orderLine) {
// BEGIN USER CODE
const quantity = orderLine.get("Quantity");
const unitPrice = orderLine.get("UnitPrice"); // Returns a Big decimal
const total = unitPrice.times(quantity);
orderLine.set("LineTotal", total);
return orderLine;
// END USER CODE
}Common object operations:
| Operation | Code | Notes |
|---|---|---|
| Get attribute | obj.get("AttrName") |
Returns the native JS type or Big for decimals. |
| Set attribute | obj.set("AttrName", value) |
Modifies the object in the client cache. |
| Get GUID | obj.getGuid() |
The object's unique identifier. |
| Get entity type | obj.getEntity() |
Returns the fully qualified entity name (e.g., MyModule.Customer). |
| Check if attribute has changed | obj.isAttributeReadOnly("AttrName") |
Useful for conditional logic. |
Lists passed to JavaScript actions are standard JavaScript arrays of Mendix objects:
export async function FilterHighValueOrders(orderList, threshold) {
// BEGIN USER CODE
return orderList.filter(order => {
const total = order.get("TotalAmount");
return total.gte(threshold); // Big.js comparison
});
// END USER CODE
}You can import npm packages or local JavaScript modules:
// BEGIN EXTRA CODE
import { format } from "date-fns";
// END EXTRA CODE
export async function FormatDateLocalized(dateValue, formatPattern) {
// BEGIN USER CODE
if (!dateValue) return "";
return format(new Date(dateValue), formatPattern);
// END USER CODE
}To include an npm package:
- Add it to the
package.jsonin your Mendix project'sjavascriptsourcefolder (or thethemesourcefolder depending on your project structure). - Run
npm installin that directory. - Import it in the
BEGIN EXTRA CODEsection.
JavaScript actions give nanoflows access to everything the browser provides:
| Use Case | Browser API | Example |
|---|---|---|
| Geolocation | navigator.geolocation |
Get the user's GPS coordinates. |
| Clipboard | navigator.clipboard |
Copy/paste text. |
| Local Storage | window.localStorage |
Persist small amounts of data outside the Mendix cache. |
| Camera (mobile) | navigator.mediaDevices.getUserMedia() |
Access the device camera directly. |
| Notifications | Notification API |
Show browser push notifications. |
| Speech | SpeechRecognition / speechSynthesis |
Voice input / text-to-speech. |
| Vibration | navigator.vibrate() |
Haptic feedback on mobile. |
| Network status | navigator.onLine |
Check if the device has connectivity. |
| Screen orientation | screen.orientation |
Detect or lock screen orientation. |
Example -- checking network status:
export async function IsDeviceOnline() {
// BEGIN USER CODE
return navigator.onLine;
// END USER CODE
}If you need to return multiple values from a JavaScript action, you have two options:
- Modify an input object's attributes (pass a Mendix object, set multiple attributes on it, return it).
- Return a single value and call the action multiple times (less efficient but sometimes clearer).
- Create a non-persistable entity specifically for the return value, pass it in, populate it, return it.
Option 1 or 3 is almost always preferable. Avoid making multiple round-trips when one will do.
Modern JavaScript actions should use async/await:
export async function GetGPSCoordinates(locationObject) {
// BEGIN USER CODE
return new Promise((resolve, reject) => {
if (!navigator.geolocation) {
reject(new Error("Geolocation is not supported by this browser."));
return;
}
navigator.geolocation.getCurrentPosition(
(position) => {
locationObject.set("Latitude", new Big(position.coords.latitude.toString()));
locationObject.set("Longitude", new Big(position.coords.longitude.toString()));
locationObject.set("Accuracy", new Big(position.coords.accuracy.toString()));
resolve(locationObject);
},
(error) => {
reject(new Error("GPS error: " + error.message));
},
{
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 0
}
);
});
// END USER CODE
}If your JavaScript action returns a Promise, the nanoflow will automatically wait for it to resolve before continuing to the next activity.
Nanoflows are ideal for controlling the user interface because they run on the client -- where the UI lives. There is no round-trip delay between deciding to show a page and the page actually appearing.
The "Show Page" activity in a nanoflow opens a page immediately. You can pass objects as page parameters.
Options:
| Setting | Description | Typical Use |
|---|---|---|
| Page | The page to open | Select from your project's pages. |
| Page object | The object to pass as the page parameter | The page's data view receives this object. |
| Page title | Override the page title dynamically | Useful for context-dependent titles (e.g., "Edit Order #1234"). |
| Location | Content (replace current), Popup, or Modal | Content for main navigation; popups for auxiliary screens. |
Modal dialog pattern:
[Start]
--> Create Object: ConfirmationHelper (non-persistable)
- Message = "Are you sure you want to delete this order?"
- ConfirmAction = "Delete"
--> Show Page: ConfirmationDialog (modal)
- Pass $ConfirmationHelper
--> [End]
The nanoflow on the confirmation dialog's "Yes" button then executes the actual delete logic.
"Close Page" closes the current page. In a popup or modal context, it closes the popup and returns control to the underlying page. You can optionally specify how many levels to close.
Multi-level close:
If you opened Page A > Page B > Page C, calling Close Page with numberOfPagesToClose = 2 from Page C will close both C and B, returning to A.
Displays a blocking dialog box with a message. The nanoflow pauses until the user dismisses it.
| Message Type | Icon | When to Use |
|---|---|---|
| Information | Blue "i" icon | Confirmations, status updates |
| Warning | Yellow triangle | Non-critical issues, "are you sure?" prompts |
| Error | Red circle | Failures, validation errors (when a dialog is appropriate) |
Avoid overusing blocking dialogs. Users find them disruptive. For non-critical feedback, prefer validation messages or inline notifications.
Displays a validation message on a specific widget -- typically a text input field. This is the gold standard for form validation in nanoflows because it highlights exactly which field has the problem.
[Start]
--> Decision: $Order/CustomerName is empty?
- True --> Validation Feedback
- Object: $Order
- Member: CustomerName
- Message: "Customer name is required."
- End: false (continue checking other fields)
- False --> (continue)
--> Decision: $Order/TotalAmount <= 0?
- True --> Validation Feedback
- Object: $Order
- Member: TotalAmount
- Message: "Order total must be greater than zero."
- End: false
- False --> (continue)
--> Decision: $HasErrors?
- True --> [End: false]
- False --> Call Microflow: ACT_Order_Save
--> [End: true]
Nanoflows handle navigation cleanly:
| Activity | What It Does | Use Case |
|---|---|---|
| Show Page | Opens a specific page (replace content, popup, or modal) | Primary navigation |
| Close Page | Closes current page / popup / modal | Cancel, done, back |
| Show Home Page | Navigates to the app's home page | After logout, on error, "go home" button |
| Sign Out | Signs the user out and redirects to the login page | Logout button |
Conditional navigation:
[Start]
--> Decision: $User/Role = 'Admin'?
- True --> Show Page: AdminDashboard
- False --> Decision: $User/Role = 'Manager'?
- True --> Show Page: ManagerDashboard
- False --> Show Page: EmployeeDashboard
--> [End]
This runs instantly because the decision logic is client-side. No server call needed to decide which page to show (assuming the role information is already on the client).
For nanoflows that include a microflow call (which introduces a network delay), you can wrap the call with progress indicators:
[Start]
--> Show Progress: "Saving your changes..."
--> Call Microflow: ACT_SaveOrder
--> Hide Progress
--> Close Page
--> [End]
Best practices for progress indicators:
- Always pair Show Progress with Hide Progress. If an error occurs between them, the progress indicator will stick. Use error handling (covered in Section 9) to ensure Hide Progress runs on all paths.
- Keep progress messages short and action-oriented ("Saving...", "Loading data...", "Syncing...").
- Do not show progress for operations that take under 200ms. It creates a distracting flash.
After a microflow returns data that has been changed on the server, the client cache might be stale. You have several options:
- Return the modified object from the microflow -- the client cache updates automatically.
- Use the "Refresh in Client" option on the microflow's commit activity -- this tells the client to refresh all widgets displaying that object.
- Call the "Refresh Object" JavaScript action from the nanoflow for finer control.
Nanoflows run on the browser's main thread -- the same thread that handles rendering, user input, animations, and every other piece of JavaScript on the page. If your nanoflow takes 500ms to execute, the browser freezes for 500ms. No scrolling, no button clicks, no animations. The UI is dead until the nanoflow finishes.
This is not a problem for typical nanoflows (which complete in under 5ms). It becomes a problem when developers treat nanoflows like microflows and try to do heavy work on the client.
| Guideline | Why |
|---|---|
| Avoid looping over more than a few hundred objects | Each iteration is a synchronous JavaScript operation. 10,000 iterations can block the UI for seconds. |
| Do not sort or filter very large lists in nanoflows | Use a microflow to sort/filter on the server and return the result. |
| Keep nanoflow depth shallow | Nanoflow A calling B calling C calling D calling E creates a deep call stack. Each call adds overhead. Keep it to 2-3 levels max. |
| Avoid string concatenation in large loops | JavaScript string operations create new string objects each time. In a loop, this causes excessive garbage collection. |
| Do not use nanoflows for file processing | Parsing CSVs, processing images, or manipulating binary data should happen on the server. |
| Return only what you need from microflows | If a microflow returns 5,000 objects but the nanoflow only needs 50, add filtering on the server side. Every object returned is serialized, sent over the network, and deserialized in the client. |
Each "Call Microflow" from a nanoflow is an HTTP request. On a fast network, that is 50-200ms. On a slow mobile connection, that is 500-2000ms.
Anti-pattern: Multiple sequential microflow calls
[Start]
--> Call Microflow: GetCustomerDetails (200ms)
--> Call Microflow: GetCustomerOrders (200ms)
--> Call Microflow: GetCustomerPreferences (200ms)
--> Call Microflow: GetCustomerBalance (200ms)
--> Show Page: CustomerDashboard
[End]
Total: ~800ms of waiting
Better pattern: Single microflow that returns everything
[Start]
--> Call Microflow: GetCustomerDashboardData (250ms)
- Returns: CustomerDashboardDTO (non-persistable entity
with associations to all needed data)
--> Show Page: CustomerDashboard
[End]
Total: ~250ms of waiting
Create a non-persistable entity (or a set of them) that bundles all the data the page needs. Fetch it in one round-trip.
| Method | How |
|---|---|
| Browser dev tools Performance tab | Record a trace, look for long-running JavaScript tasks during nanoflow execution. |
| Console timing | In a JavaScript action, use console.time("myLabel") / console.timeEnd("myLabel") to measure specific operations. |
| Network tab | Watch for microflow calls (XHR requests to /xas/). Check their timing and payload size. |
| Mendix built-in profiling | In Studio Pro's Run > Advanced menu, enable client-side profiling to see nanoflow execution times. |
| Operation | Nanoflow | Microflow | Winner |
|---|---|---|---|
| Toggle a boolean attribute | < 1ms | 50-200ms (network + server) | Nanoflow |
| Validate 5 form fields | < 5ms | 50-200ms | Nanoflow |
| Sort a list of 50 items | < 5ms | 50-200ms | Nanoflow |
| Sort a list of 10,000 items | 200-500ms (UI blocks) | 60-220ms (server is fast at this) | Microflow |
| Filter 100 objects by a string attribute | < 10ms | 50-200ms | Nanoflow |
| Full-text search across 100,000 records | Not feasible | 100-500ms (with DB indexes) | Microflow |
| Calculate aggregate over 5,000 records | 50-200ms (UI blocks) | 60-220ms | Microflow (no UI blocking) |
| Show a page | < 5ms | 50-200ms | Nanoflow |
| Navigate to the next step in a wizard | < 5ms | 50-200ms | Nanoflow |
Do not load all data upfront. Load what is visible, then load more on demand:
[Start: Page Load]
--> Call Microflow: GetFirst20Orders
- Returns: $OrderList
--> Change Variable: $PageNumber = 1
--> [End]
[Load More Button]
--> Show Progress: "Loading..."
--> Change Variable: $PageNumber = $PageNumber + 1
--> Call Microflow: GetOrdersPage
- Parameter: $PageNumber
- Returns: $MoreOrders
--> List Operation: $OrderList = $OrderList + $MoreOrders
--> Hide Progress
--> [End]
Nanoflows support structured error handling, similar to try/catch in programming languages. When an activity fails (a microflow call returns an error, a JavaScript action throws an exception, or a validation fails), you can catch that error and respond gracefully instead of showing the user a generic red error banner.
Every activity in a nanoflow has an "Error Handling" setting with three options:
| Option | Behavior | When to Use |
|---|---|---|
| Abort (default) | The nanoflow stops immediately. A generic error message is shown to the user. | Only for truly unexpected errors where you have no recovery strategy. |
| Custom without rollback | The nanoflow continues along a custom error path. Object changes are preserved. | When you want to handle the error and keep any partial work done so far. |
| Custom with rollback | The nanoflow continues along a custom error path. Object changes made in this nanoflow are reverted. | When partial changes would leave the data in an inconsistent state. |
The visual equivalent of try/catch:
[Start]
--> Show Progress: "Saving..."
--> Call Microflow: ACT_SaveOrder <-- Error handling: Custom without rollback
| |
| (success path) | (error path)
v v
--> Hide Progress --> Hide Progress
--> Close Page --> Log Message: $latestError
--> [End: success] --> Show Message: "Could not save.
Please check your connection
and try again."
--> [End: failure]
The $latestError variable is automatically available on the error path. It contains the error message from the failed activity.
When an activity fails and you have custom error handling configured, Mendix provides a special variable:
| Variable | Type | Content |
|---|---|---|
$latestError |
String | The error message from the failed activity |
$latestSoapFault |
SoapFault object | Only available for failed web service calls (not common in nanoflows) |
$latestError contains the raw error message. For microflow calls, this is whatever error message the microflow generated (either from a throw or an unhandled exception). For JavaScript actions, this is the message from the thrown Error object.
Understanding how errors propagate through nested nanoflows:
| Situation | Behavior |
|---|---|
| Sub-nanoflow throws, outer nanoflow has error handling on the call | The outer nanoflow catches the error and follows the error path. |
| Sub-nanoflow throws, outer nanoflow does NOT have error handling | The error propagates up. If no handler is found, the user sees a generic error dialog. |
| Microflow throws, nanoflow has error handling on the call | The nanoflow catches it. $latestError contains the microflow's error message. |
| JavaScript action throws, nanoflow has error handling | The nanoflow catches it. $latestError contains the Error object's message. |
| JavaScript action rejects a Promise, nanoflow has error handling | Same as above -- rejected promises are caught by the nanoflow's error handler. |
Do not just catch errors silently. The user needs to know what happened and what to do about it.
Good error feedback patterns:
| Situation | Feedback | Implementation |
|---|---|---|
| Server unreachable | "Unable to reach the server. Your changes are saved locally and will sync when you reconnect." | Show Message (Warning) |
| Validation error from server | Show the specific validation message on the relevant field | Validation Feedback activity |
| Permission denied | "You don't have permission to perform this action. Contact your administrator." | Show Message (Error) |
| Timeout | "The request took too long. Please try again." | Show Message (Warning) |
| Unknown error | "Something went wrong. Please try again. If the problem persists, contact support." + Log the full error for developers | Show Message (Error) + Log Message |
Anti-patterns:
- Catching an error and doing nothing (silent failure). The user thinks the action succeeded.
- Showing the raw
$latestErrorto the user. It often contains technical details (stack traces, SQL errors) that confuse users and may leak sensitive information. - Not logging the error. Even if you show a friendly message to the user, log the technical details to the console for debugging.
A common bug: Show Progress is called, then a microflow fails, and Hide Progress is never called. The user is stuck with a permanent spinner.
Always ensure Hide Progress is called on both success and error paths:
[Start]
--> Show Progress: "Processing..."
--> Call Microflow: ACT_ProcessData <-- Error handling: Custom without rollback
| |
| (success) | (error)
v v
--> Hide Progress --> Hide Progress
--> Show Message: "Done." --> Show Message: "Processing failed."
--> [End] --> [End]
In JavaScript actions, throw standard JavaScript errors:
export async function ValidateCustomFormat(inputString, formatPattern) {
// BEGIN USER CODE
if (!inputString) {
throw new Error("Input string is required.");
}
const regex = new RegExp(formatPattern);
if (!regex.test(inputString)) {
throw new Error(
"Input '" + inputString + "' does not match the expected format."
);
}
return true;
// END USER CODE
}The nanoflow's error handler catches this and puts the Error message into $latestError.
For async operations, reject the promise or let the async function throw:
export async function FetchExternalData(url) {
// BEGIN USER CODE
const response = await fetch(url);
if (!response.ok) {
throw new Error("HTTP " + response.status + ": " + response.statusText);
}
return await response.text();
// END USER CODE
}Everything in a nanoflow runs on the client. The client is the user's browser. The user controls their browser. This means:
Anything in a nanoflow is visible to the user.
This is not a theoretical concern. It is trivially easy. A user opens browser dev tools, looks at the JavaScript source, and reads your nanoflow logic. Mendix compiles nanoflows to JavaScript, and while it is not pretty, it is readable enough for someone motivated to extract business rules, conditions, and string literals from it.
| Nanoflow Contains | Risk | What to Do Instead |
|---|---|---|
| API keys or secrets | User extracts the key and uses it independently, potentially incurring costs or accessing data they should not | Store secrets server-side. Pass them only to microflows. Never hardcode them in nanoflows or JavaScript actions. |
| Pricing calculation logic | Competitor or customer reverse-engineers your pricing model | Calculate prices in a microflow. Return only the result to the client. |
| Discount eligibility rules | User figures out how to qualify for discounts they should not receive | Evaluate discount eligibility server-side. |
| Authorization/permission checks | User bypasses the check by modifying the client-side JavaScript | Always enforce permissions server-side (entity access rules + microflow security). Nanoflow checks are UX conveniences, not security barriers. |
| Feature flag logic | User discovers unreleased features or enables features they should not have | Evaluate feature flags on the server. Return only the boolean result. |
| Hardcoded URLs to internal systems | User discovers internal infrastructure endpoints | Keep internal URLs in server-side constants. |
| Sensitive business rules (underwriting, risk scoring, fraud detection) | User understands and games the system | All sensitive logic must run in microflows. |
Treat nanoflow security checks as UX helpers, not as actual security:
Layer 1 -- Nanoflow (UX):
[Start]
--> Decision: $CurrentUser/IsManager?
- True --> Show Page: ApprovalForm
- False --> Show Message: "Only managers can approve requests."
--> [End]
This provides a good user experience -- non-managers immediately see a helpful message instead of getting an error after a server round-trip.
Layer 2 -- Microflow + Entity Access (Actual Security):
The ApprovalForm's save button calls a microflow. That microflow checks the user's role server-side. The entity access rules prevent non-managers from modifying the approval status. Even if someone bypasses the nanoflow check (by manipulating the client), the server rejects the action.
Both layers are needed. The nanoflow provides instant feedback. The server provides actual enforcement. Never rely on only one.
Even though nanoflows manipulate objects on the client, entity access rules are enforced at the server boundary:
| Operation | Where Access Rules Are Checked |
|---|---|
| Nanoflow creates/changes an object locally | No access rule check (it is in the client cache only) |
| Nanoflow calls a microflow that commits the object | Server checks entity access rules before commit |
| Offline app syncs changes to the server | Server checks entity access rules during sync |
| Nanoflow retrieves objects from cache | Only objects that passed access rules when loaded are in the cache |
The practical effect: a user could theoretically modify any attribute on a client-side object in their browser's memory, but the moment that object is sent to the server (via microflow commit or sync), the server's access rules reject unauthorized changes.
| Storage Location | Visibility | Risk |
|---|---|---|
| Client object cache (in-memory) | Visible via browser dev tools (Memory tab) | User can inspect any object currently loaded |
| IndexedDB / Local Storage | Visible via browser dev tools (Application tab) | Persistent data visible to anyone with access to the browser |
| Offline SQLite database (mobile) | Accessible if device is rooted/jailbroken | All offline-synced data can be extracted |
| JavaScript source (nanoflow logic) | Visible via browser dev tools (Sources tab) | All nanoflow conditions, constants, and logic readable |
| Check | Action |
|---|---|
| Does the nanoflow contain any hardcoded strings that are sensitive? | Move them to server-side constants, access via microflow. |
| Does the nanoflow make authorization decisions? | Ensure the same check exists server-side (microflow or entity access). |
| Does the nanoflow calculate prices, totals, or financial values? | Either calculate server-side, or validate the result server-side before committing. |
| Could a user benefit from modifying the local object before it syncs/commits? | Add server-side validation in the commit microflow. |
| Does a JavaScript action reference internal URLs or service endpoints? | Route calls through a server-side microflow instead. |
| Is any PII (personal data) being processed in nanoflow logic beyond what is displayed? | Minimize PII in client-side logic. Process it server-side. |
If a nanoflow calculates a total, a discount, a tax amount, or any value that affects money or permissions, recalculate it on the server before committing.
Nanoflow (UX, instant feedback):
--> Change $Order/EstimatedTotal = calculated value
--> Show updated total on the page
Microflow (actual save):
--> Recalculate TotalAmount from line items (server-side)
--> Compare with what the client sent
--> If they match: commit
--> If they differ: log a warning, commit the server-calculated value
Problem: You want to validate a form before submitting to the server. Server validation works but takes 200ms+ per attempt, which is frustrating on complex forms.
Solution: Validate locally in a nanoflow, then call a microflow only when the form is valid.
[Start]
--> Create Variable: $IsValid (Boolean) = true
--> Decision: $Customer/Name is empty?
- True --> Validation Feedback on $Customer/Name: "Name is required."
Change Variable: $IsValid = false
- False --> (continue)
--> Decision: $Customer/Email is empty?
- True --> Validation Feedback on $Customer/Email: "Email is required."
Change Variable: $IsValid = false
- False --> Decision: $Customer/Email does not match email pattern?
- True --> Validation Feedback on $Customer/Email: "Enter a valid email address."
Change Variable: $IsValid = false
- False --> (continue)
--> Decision: $Customer/PhoneNumber length < 10?
- True --> Validation Feedback on $Customer/PhoneNumber: "Phone number must be at least 10 digits."
Change Variable: $IsValid = false
- False --> (continue)
--> Decision: $IsValid?
- True --> Call Microflow: ACT_Customer_Save
--> Close Page
- False --> [End] (stay on page, validation messages are shown)
Key details:
- Do not stop at the first error. Check every field so the user can fix all issues at once.
- Use the
$IsValidflag pattern to track overall form validity without short-circuiting. - Do not rely solely on client-side validation. The microflow should also validate, because a malicious user can skip the nanoflow entirely.
Problem: The user clicks a "like" button or toggles a favorite. You want the UI to respond instantly, but the change must persist on the server.
Solution: Update the UI immediately via the nanoflow, then call a microflow to persist the change. If the microflow fails, revert.
[Start]
--> Change Object: $Post
- IsLikedByCurrentUser = not($Post/IsLikedByCurrentUser)
- LikeCount = if $Post/IsLikedByCurrentUser
then $Post/LikeCount + 1
else $Post/LikeCount - 1
--> Call Microflow: ACT_Post_ToggleLike <-- Error handling: Custom without rollback
| |
| (success) | (error)
v v
--> [End] --> Change Object: $Post (revert)
- IsLikedByCurrentUser = not($Post/IsLikedByCurrentUser)
- LikeCount = (revert count)
--> Show Message: "Could not save. Please try again."
--> [End]
The user sees the like count change instantly. If the server call fails, the change is reverted and the user is notified.
Problem: A section of the page should show or hide based on a user's selection in a dropdown or checkbox.
Solution: Use a non-persistable helper entity with boolean attributes that control visibility.
Setup:
- Create a non-persistable entity:
PageHelperwith attributesShowAdvancedOptions(Boolean),ShowShippingAddress(Boolean), etc. - On the page's data view, create the
PageHelperobject via a nanoflow. - Bind conditional visibility on page sections to these attributes.
- When the user changes a value, an on-change nanoflow toggles the relevant boolean.
On-change nanoflow:
[Start: $Customer/OrderType changed]
--> Decision: $Customer/OrderType = 'Shipping'?
- True --> Change Object: $PageHelper
- ShowShippingAddress = true
- False --> Change Object: $PageHelper
- ShowShippingAddress = false
--> [End]
Why use a helper entity instead of the actual entity?
- You do not pollute your domain model with UI-only attributes.
- The helper is non-persistable, so it is never committed to the database.
- Multiple visibility conditions can be controlled independently.
Problem: You have a list of objects displayed on the page and want the user to filter or sort them without a server call.
Solution: Use nanoflows to filter and sort the client-side list.
Filtering:
[Start: Search text changed]
--> Decision: $SearchText is empty?
- True --> Change Variable: $DisplayedList = $FullList
- False --> Create List: $FilteredList (empty)
--> Loop: $Item in $FullList
--> Decision: $Item/Name contains $SearchText
(case-insensitive)
- True --> List Operation:
Add $Item to $FilteredList
- False --> (continue)
--> Change Variable: $DisplayedList = $FilteredList
--> [End]
Sorting:
For sorting, you can use a nanoflow "Sort" activity on a list, specifying the attribute and direction. If you need multi-column sorting or custom sort logic, a JavaScript action is more practical:
export async function SortByAttribute(list, attributeName, ascending) {
// BEGIN USER CODE
return list.sort((a, b) => {
const valA = a.get(attributeName);
const valB = b.get(attributeName);
if (valA === valB) return 0;
if (valA === null || valA === undefined) return 1;
if (valB === null || valB === undefined) return -1;
const comparison = valA < valB ? -1 : 1;
return ascending ? comparison : -comparison;
});
// END USER CODE
}Performance note: Keep the list under a few hundred items for client-side filtering. For larger datasets, use a microflow with server-side filtering.
Problem: A complex data entry process needs to be broken into multiple steps with back/forward navigation.
Solution: Use a non-persistable WizardState entity to track the current step, and nanoflows for navigation between steps.
Setup:
- Create
WizardState(non-persistable):CurrentStep(Integer),TotalSteps(Integer), and associations to the data being collected at each step. - Use conditional visibility on the page to show only the widgets for the current step.
- Next/Previous buttons trigger nanoflows that change
CurrentStep.
Next button nanoflow:
[Start]
--> Call Nanoflow: ValidateCurrentStep
- Returns: $StepIsValid (Boolean)
--> Decision: $StepIsValid?
- True --> Decision: $WizardState/CurrentStep = $WizardState/TotalSteps?
- True --> Call Microflow: ACT_SubmitWizardData
--> Show Page: WizardComplete
- False --> Change Object: $WizardState
- CurrentStep = $WizardState/CurrentStep + 1
- False --> (stay on current step, validation messages shown)
--> [End]
Each step validates locally before allowing progression. The final step calls a microflow to commit everything at once.
Problem: A search field triggers a microflow on every keystroke, causing excessive server calls.
Solution: Use a JavaScript action to debounce the input, then trigger the search nanoflow only after the user stops typing.
JavaScript action -- Debounce:
// BEGIN EXTRA CODE
let debounceTimer = null;
// END EXTRA CODE
export async function DebouncedAction(delayMs) {
// BEGIN USER CODE
return new Promise((resolve) => {
if (debounceTimer) {
clearTimeout(debounceTimer);
}
debounceTimer = setTimeout(() => {
resolve(true);
}, delayMs);
});
// END USER CODE
}On-change nanoflow on the search input:
[Start: $SearchHelper/SearchText changed]
--> Call JavaScript Action: DebouncedAction
- delayMs = 300
--> Decision: $SearchHelper/SearchText length >= 3?
- True --> Show Progress: "Searching..."
--> Call Microflow: ACT_Search
- SearchTerm = $SearchHelper/SearchText
- Returns: $Results
--> Hide Progress
- False --> Change Variable: $Results = empty list
--> [End]
The nanoflow fires on every keystroke, but the debounce JavaScript action delays execution by 300ms. If the user types another character within that window, the previous call is cancelled. The microflow only fires when the user pauses.
Problem: The user clicks a delete button. You want to confirm before actually deleting.
Solution: Use a nanoflow with a decision or a confirmation popup.
Simple approach (Show Message with type Confirmation is not available in nanoflows):
Since nanoflows do not have a built-in confirmation dialog activity, use one of these alternatives:
Option A -- Custom confirmation page:
[Start]
--> Show Page: DeleteConfirmation (modal)
- Pass $Order
--> [End]
On the confirmation page, the "Yes, Delete" button runs:
[Start]
--> Show Progress: "Deleting..."
--> Call Microflow: ACT_Order_Delete
--> Hide Progress
--> Close Page
--> Close Page (close the parent page too, if needed)
--> [End]
Option B -- JavaScript action with native confirm():
export async function ShowConfirmDialog(message) {
// BEGIN USER CODE
return window.confirm(message);
// END USER CODE
}[Start]
--> Call JavaScript Action: ShowConfirmDialog
- message = "Are you sure you want to delete this order?"
- Returns: $Confirmed (Boolean)
--> Decision: $Confirmed?
- True --> Call Microflow: ACT_Order_Delete
--> Close Page
- False --> [End]
Option A looks better (styled to match your app). Option B is faster to implement and uses the browser's native dialog.
Problem: Multiple microflow calls need to execute, and the UI should reflect the current loading state without stacking multiple progress indicators.
Solution: Use a helper entity to manage loading state.
[Start: Page Load]
--> Change Object: $PageHelper
- IsLoading = true
- LoadingMessage = "Loading dashboard data..."
--> Call Microflow: ACT_GetDashboardData
- Returns: $DashboardData
--> Change Object: $PageHelper
- IsLoading = false
--> [End]
Bind the loading overlay's visibility to $PageHelper/IsLoading and bind a text widget to $PageHelper/LoadingMessage. This gives you full control over the loading UX without relying on the generic progress indicator.
Nanoflows run in the browser, which means server-side Mendix logs do not capture nanoflow execution. You need different tools.
The browser's dev tools are your primary debugging instrument for nanoflows. Here is what each tab gives you:
| Tab | What to Look For | Nanoflow Relevance |
|---|---|---|
| Console | Errors, warnings, console.log() output from JavaScript actions, Mendix client messages |
Log Message activities in nanoflows write here. JavaScript action errors appear here. |
| Network | XHR/Fetch requests to /xas/ (microflow calls), timing, payload size |
Every "Call Microflow" from a nanoflow generates an XHR request. Look for failed requests (red), slow requests, and large payloads. |
| Sources | JavaScript source code, breakpoints | Nanoflow-compiled JavaScript is here, though it is hard to read. JavaScript actions are more debuggable. Set breakpoints in your JS action code. |
| Application | IndexedDB, Local Storage, Session Storage | In offline apps, the local database is an IndexedDB database. You can browse its contents. |
| Performance | Flame charts, function execution timing | Record a trace while triggering a nanoflow to see exactly how long each step takes and whether it blocks the main thread. |
| Memory | Heap snapshots | Useful for finding memory leaks in JavaScript actions or tracking down issues with large object caches. |
The most direct way to debug nanoflow behavior is to add logging in JavaScript actions:
export async function ProcessOrder(order) {
// BEGIN USER CODE
console.log("ProcessOrder called with:", {
guid: order.getGuid(),
status: order.get("Status"),
total: order.get("TotalAmount")?.toString()
});
// ... processing logic ...
console.log("ProcessOrder result:", {
newStatus: order.get("Status"),
isValid: result
});
return result;
// END USER CODE
}Use console.group() / console.groupEnd() to organize complex debugging output:
console.group("Order Validation");
console.log("Checking required fields...");
// ... checks ...
console.log("Checking business rules...");
// ... checks ...
console.groupEnd();Studio Pro includes a nanoflow debugger that works similarly to the microflow debugger:
- Set breakpoints: Click the line between two activities to set a breakpoint (red circle appears).
- Run the app locally: The app runs in your browser as usual.
- Trigger the nanoflow: Click the button or perform the action that fires the nanoflow.
- Studio Pro pauses execution: The debugger highlights the current activity. You can inspect variables and objects.
- Step through: Use Step Into, Step Over, and Continue to walk through the nanoflow.
Debugger panels:
| Panel | Shows |
|---|---|
| Variables | Current values of all variables in scope (strings, numbers, booleans, etc.) |
| Current Object | Attributes and associations of the object in context |
| Call Stack | The chain of nanoflow calls if you are inside a sub-nanoflow |
| Breakpoints | All active breakpoints across all nanoflows |
Limitations of the Studio Pro nanoflow debugger:
- It requires the app to run locally (cannot debug a deployed app).
- It works only in the browser, not in native mobile apps (use Chrome DevTools for that via remote debugging).
- JavaScript action internals are not visible in the Studio Pro debugger. For those, use browser dev tools.
- Debugging pauses the nanoflow execution, which can cause timeouts on any pending microflow calls.
Offline apps add complexity because data lives in a local database:
| Technique | How |
|---|---|
| Inspect IndexedDB | Open browser dev tools > Application > IndexedDB. Browse the local database tables to see what data is synced. |
| Monitor sync requests | Watch the Network tab during sync. Look at the requests to /xas/ endpoints. Check for errors. |
| Force sync | Add a temporary "Debug Sync" button that triggers a Synchronize activity. Use this to test sync behavior on demand. |
| Check sync errors | Sync errors appear in the browser console. Look for conflict messages, access rule violations, or validation failures. |
| Clear local data | In the browser's Application tab, you can delete the IndexedDB database to simulate a first-time install. |
| Issue | Symptom | Diagnosis | Fix |
|---|---|---|---|
| Nanoflow does not fire | Nothing happens when the user clicks the button | Check the button's on-click event is set to the nanoflow. Check conditional visibility -- the button might be hidden. Check browser console for JavaScript errors. | Fix the event binding or resolve the JS error. |
| Retrieve returns empty | The nanoflow logic expects objects but gets an empty list | The objects are not in the client cache. In online apps, they may not have been loaded by the page. In offline apps, they may not be in the sync scope. | Ensure the page loads the needed objects, or use a microflow to fetch them. |
| Object attribute is null/undefined | A decision or calculation fails because an attribute has no value | The attribute was not set before this point, or the object was not fully loaded. | Add null checks in decisions. Ensure objects are fully loaded before processing. |
| Microflow call fails silently | The nanoflow appears to hang or skip steps after a microflow call | The microflow threw an error and the nanoflow has no error handling. | Add custom error handling to the microflow call activity. |
| Progress indicator never disappears | The app shows a permanent spinner | A nanoflow called Show Progress but the Hide Progress was skipped (error path not handled). | Ensure Hide Progress is on every possible path, including error paths. |
| Page shows stale data after a microflow call | The microflow changed data on the server, but the page still shows old values | The client cache was not refreshed. | Return the modified object from the microflow, or enable "Refresh in Client" on the commit. |
| Performance degrades over time | The app gets slower the longer it is open | Memory leak in a JavaScript action, or the client cache is growing unbounded. | Profile with the Memory tab. Check for event listeners that are not cleaned up. |
| Offline sync fails | Sync shows an error or objects do not appear after sync | Access rule violations, validation errors on the server, or sync scope misconfiguration. | Check the browser console for sync error details. Review entity access and sync configuration. |
| Nanoflow works locally but not in production | Logic behaves differently after deployment | Different data state, missing objects in cache, or different timing. | Compare the client-side data state. Check that all referenced constants and enumerations are deployed. |
| JavaScript action error not caught | Generic error banner appears instead of your custom error handling | The error handling on the JavaScript action call is still set to "Abort" (default). | Change it to "Custom without rollback" and add an error path. |
For nanoflows you expect to debug often, add explicit logging:
[Start]
--> Log Message: "NF_ValidateOrder started. Order ID: " + $Order/OrderNumber
--> (... validation logic ...)
--> Log Message: "NF_ValidateOrder result: IsValid = " + toString($IsValid)
--> [End]
These Log Messages go to the browser console (console.log()). They persist across page navigations until the user clears the console or closes the browser.
For production apps, consider wrapping diagnostic logs in a condition:
[Start]
--> Decision: $AppSettings/EnableDebugLogging?
- True --> Log Message: "NF_ValidateOrder diagnostic info..."
- False --> (skip logging)
--> (... rest of the nanoflow ...)
This lets you enable verbose logging without deploying code changes.
For Mendix native mobile apps running on a physical device:
| Platform | Tool | Setup |
|---|---|---|
| Android | Chrome DevTools (remote debugging) | Enable USB debugging on the device. Connect via USB. Open chrome://inspect in Chrome on your computer. |
| iOS | Safari Web Inspector | Enable Web Inspector on the iOS device (Settings > Safari > Advanced). Connect via USB. Open Safari on your Mac and find the device under Develop menu. |
| Both | Mendix Make It Native app | Connects to your locally running app. Errors appear in your local Studio Pro console and the device's remote dev tools. |
When a nanoflow is not working as expected, go through this checklist:
- Open browser console -- look for red errors or yellow warnings.
- Check the Network tab -- are microflow calls succeeding? Look for HTTP 4xx/5xx responses.
- Verify object state -- use a Log Message or JavaScript action to print the current object's attributes.
- Check conditional paths -- add Log Messages before and after each decision to see which path is taken.
- Check for null values -- the most common nanoflow bug is an unexpected null/empty attribute.
- Test with breakpoints -- if you can reproduce locally, use the Studio Pro nanoflow debugger.
- Isolate the problem -- create a minimal nanoflow that reproduces the issue. Remove complexity until you find the offending activity.
- Check data availability -- especially for retrieves. Is the data actually in the client cache or local DB?
- Review error handling -- ensure all microflow calls and JavaScript action calls have custom error handling configured.
- Check timing -- if the issue is intermittent, it may be a race condition. Look for async operations completing out of order.
| Category | Works in Nanoflow | Microflow Only |
|---|---|---|
| Object | Create, Change, Delete, Retrieve, Rollback | Commit |
| List | All (Aggregate, Create, Filter, Sort, etc.) | -- |
| Flow | Decision, Merge, Loop, Break, Continue | -- |
| Calls | Call Nanoflow, Call Microflow, Call JS Action | Call Java Action, Call REST, Call SOAP, Call OData |
| Client | Show/Close Page, Show Message, Validation, Show/Hide Progress, Synchronize, Sign Out, Log Message | Download File, Generate Document |
| Integration | -- | REST, SOAP, XML Mapping, Email |
Do I need this to work offline?
YES --> Nanoflow (mandatory)
NO --> Does this logic contain secrets or sensitive business rules?
YES --> Microflow
NO --> Does it need database access beyond what's on the client?
YES --> Microflow (or nanoflow calling a microflow)
NO --> Does it need to be instant (< 50ms)?
YES --> Nanoflow
NO --> Either works. Prefer nanoflow for
UI logic, microflow for data logic.
Consistent naming makes your project navigable. A common convention:
| Type | Prefix | Example |
|---|---|---|
| Nanoflow (general) | NF_ |
NF_ValidateOrderForm |
| Nanoflow (on-change event) | OCH_ |
OCH_Customer_EmailChanged |
| Nanoflow (on-click event) | ACT_ (or NF_) |
ACT_Order_Delete |
| Nanoflow (page data source) | DS_ |
DS_GetCurrentOrder |
| Nanoflow (on-page-load) | OPL_ |
OPL_InitializeDashboard |
| JavaScript Action | JSA_ |
JSA_CopyToClipboard |
The exact convention does not matter as much as consistency. Pick one and stick with it across the project.
- Mendix Documentation -- Nanoflows -- Official reference for all nanoflow activities and properties.
- Mendix Documentation -- JavaScript Actions -- How to create and use custom JavaScript actions.
- Mendix Documentation -- Offline-First -- Complete guide to offline-first app development.
- Mendix Documentation -- Client Activities -- Reference for Show Page, Show Message, and other client activities.
- Mendix Documentation -- Nanoflow Error Handling -- Official documentation on error handling in nanoflows.
- Mendix Documentation -- Data Synchronization -- How offline data sync works, including conflict resolution.