From 2fbac37fd54f119837281324b4d51f460e0015fc Mon Sep 17 00:00:00 2001
From: Mike Meesseman <31196959+mmeesseman@users.noreply.github.com>
Date: Thu, 19 Mar 2026 11:33:12 -0400
Subject: [PATCH 01/11] Add example for enforcing daily sync with LOADRECORDS
This document provides an example of enforcing a daily sync requirement for mobile users using LOADRECORDS. It outlines the setup and code needed to ensure users sync their devices before collecting data.
---
.../enforce-daily-sync-with-loadrecords.md | 140 ++++++++++++++++++
1 file changed, 140 insertions(+)
create mode 100644 docs/DATA EVENTS/data-events-examples/enforce-daily-sync-with-loadrecords.md
diff --git a/docs/DATA EVENTS/data-events-examples/enforce-daily-sync-with-loadrecords.md b/docs/DATA EVENTS/data-events-examples/enforce-daily-sync-with-loadrecords.md
new file mode 100644
index 00000000..e5c0bc98
--- /dev/null
+++ b/docs/DATA EVENTS/data-events-examples/enforce-daily-sync-with-loadrecords.md
@@ -0,0 +1,140 @@
+---
+title: Enforce a daily sync using LOADRECORDS
+excerpt: >-
+ This example shows how to enforce a daily sync requirement for mobile users.
+ When a record is opened, it checks a "heartbeat" Task record to confirm the
+ device has synced today. If not, it hides all fields and blocks saving until
+ the user syncs Fulcrum.
+deprecated: false
+hidden: false
+metadata:
+ title: ''
+ description: ''
+ robots: noindex
+next:
+ description: ''
+---
+
+# Enforce a daily sync using LOADRECORDS
+
+This Data Event uses a recurring Fulcrum **Task** as a daily "heartbeat." When a user opens a record, it queries the latest Task via `LOADRECORDS` and compares its due date to today's date. If they don't match — meaning the device hasn't synced today — all fields are hidden and the record cannot be saved until the user syncs.
+
+This pattern is useful for organizations that need to ensure field workers are online and up to date with any form changes, permission updates, or removals before they can collect data.
+
+## Setup
+
+Before adding the Data Event, create a recurring daily Task in your Fulcrum org:
+
+1. Navigate to **Tasks** in your Fulcrum org.
+2. Create a new Task with today's date as the due date.
+3. Set it to recur **daily**.
+4. Note the **Form ID** of the Tasks form (found in the URL when viewing the form) — you will need it in the code below.
+5. Note the **field key** for the `due_date` field in the Tasks form. You can find field keys in the form builder or via the API.
+
+> **Note:** Any user with a custom role in Fulcrum may need **ITA Access** enabled for their role in order for `LOADRECORDS` to return Task records. Check with your Fulcrum admin.
+
+## Data Event Code
+
+```js
+// Initialize persistent storage to cache the last known sync date
+let storage = STORAGE();
+const TASK_DATE_KEY = 'latest_task_date';
+
+/**
+ * Returns today's date as a YYYY-MM-DD string,
+ * matching the format stored in Fulcrum Task due_date fields.
+ */
+function getTodayString() {
+ let today = new Date();
+ let yyyy = today.getFullYear();
+ let mm = String(today.getMonth() + 1).padStart(2, '0');
+ let dd = String(today.getDate()).padStart(2, '0');
+ return `${yyyy}-${mm}-${dd}`;
+}
+
+/**
+ * Queries the most recent Task record and checks whether
+ * its due date matches today. If not, locks down the form.
+ */
+function checkSyncStatus() {
+ LOADRECORDS({
+ // Replace this form_id with the Form ID of your Fulcrum Tasks form
+ form_id: 'YOUR-TASKS-FORM-ID-HERE',
+ order: [['due_date', 'desc']],
+ limit: 1
+ }, (err, records) => {
+
+ // 1. Handle permission errors first (e.g. user lacks ITA access)
+ if (err) {
+ ALERT('Access Denied', 'Please contact your Admin for appropriate permissions to continue.');
+
+ // Store an error flag so validate-record always blocks saving
+ storage.setItem(TASK_DATE_KEY, 'permission_error');
+
+ // Hide all fields to lock down the form visually
+ DATANAMES().forEach(function(dataName) {
+ SETHIDDEN(dataName, true);
+ });
+
+ return; // Stop further execution
+ }
+
+ // 2. No error — compare the latest Task due date to today
+ let taskRecords = records.records;
+
+ if (taskRecords && taskRecords.length > 0) {
+ let latestTask = taskRecords[0];
+
+ // Replace 'YOUR-DUE-DATE-FIELD-KEY' with the actual field key
+ // for the due_date field in your Tasks form (e.g. '0ed3')
+ let taskDate = latestTask?.form_values?.['YOUR-DUE-DATE-FIELD-KEY'];
+
+ // Cache the task date for use in validate-record
+ storage.setItem(TASK_DATE_KEY, taskDate);
+
+ let todayStr = getTodayString();
+
+ if (taskDate === todayStr) {
+ // Device has synced today — allow the user to proceed
+ ALERT('All clear', 'Your data is up to date. You may proceed.');
+ } else {
+ // Task date doesn't match today — device needs to sync
+ ALERT(
+ 'Sync Required',
+ `Please close Fulcrum and sync your device before continuing. ` +
+ `Last sync date: ${taskDate}. Today: ${todayStr}.`
+ );
+
+ // Hide all fields to prevent data entry until synced
+ DATANAMES().forEach(function(dataName) {
+ SETHIDDEN(dataName, true);
+ });
+ }
+ }
+ });
+}
+
+// Run the sync check every time a record is opened
+ON('load-record', checkSyncStatus);
+
+// Block saving if the cached date doesn't match today
+// (handles the case where the app is left open overnight)
+ON('validate-record', function(event) {
+ let storedDate = storage.getItem(TASK_DATE_KEY);
+ let todayStr = getTodayString();
+
+ if (storedDate !== todayStr) {
+ INVALID('You must discard edits and sync Fulcrum (or request Admin permissions) prior to continuing.');
+ }
+});
+```
+
+## How it works
+
+When a record is opened (`load-record`), `checkSyncStatus` queries the latest Task in your org, sorted by due date descending. The most recent Task's due date acts as a heartbeat — it represents the last date the Task was synced to the device.
+
+- If the device has synced today, the due date will match today's date and the user is shown a green light.
+- If the device hasn't synced, the due date will be an older date. All fields are hidden and the user is prompted to sync.
+- If the LOADRECORDS call fails (e.g. due to a permissions error), the form is also locked down as a precaution.
+
+The `validate-record` handler provides a second line of defense: even if the user had the form open before midnight, they cannot save a record after a new day begins without first syncing.
From eb523ee04842d9dbffcd78a7bbe61b94786a7fdc Mon Sep 17 00:00:00 2001
From: Mike Meesseman <31196959+mmeesseman@users.noreply.github.com>
Date: Thu, 19 Mar 2026 11:35:47 -0400
Subject: [PATCH 02/11] Add example for preventing duplicate repeatables
This document provides an example of how to prevent users from creating more repeatable entries than allowed based on a choice field value. It includes code snippets demonstrating the use of event listeners and validation.
---
.../prevent-creating-duplicate-repeatables.md | 101 ++++++++++++++++++
1 file changed, 101 insertions(+)
create mode 100644 docs/DATA EVENTS/data-events-examples/prevent-creating-duplicate-repeatables.md
diff --git a/docs/DATA EVENTS/data-events-examples/prevent-creating-duplicate-repeatables.md b/docs/DATA EVENTS/data-events-examples/prevent-creating-duplicate-repeatables.md
new file mode 100644
index 00000000..7c8d871d
--- /dev/null
+++ b/docs/DATA EVENTS/data-events-examples/prevent-creating-duplicate-repeatables.md
@@ -0,0 +1,101 @@
+---
+title: Prevent creating duplicate repeatables
+excerpt: >-
+ This example shows how to prevent a user from creating more repeatable
+ entries than intended. It compares the current repeatable number against a
+ choice field value and blocks saving if the user tries to exceed it. It also
+ demonstrates the OFF() function to clean up event listeners when a repeatable
+ is unloaded.
+deprecated: false
+hidden: false
+metadata:
+ title: ''
+ description: ''
+ robots: noindex
+next:
+ description: ''
+---
+
+# Prevent creating duplicate repeatables
+
+In some workflows, you may want to limit how many entries a user can create in a repeatable section based on a choice they've made in the parent record. For example, if a user selects "Visit 1" in a choice field, they should only be able to create one repeatable entry — not two.
+
+This Data Event uses `REPEATABLENUMBER()`, `CHOICEVALUE()`, and the `OFF()` function to enforce that limit and properly clean up event listeners when the repeatable is closed.
+
+## Data Event Code
+
+```js
+/**
+ * Validation callback used to block saving an over-limit repeatable.
+ * Stored as a named function so it can be detached with OFF() later.
+ */
+const invalidateFunction = () => {
+ INVALID('This repeatable cannot be saved. It exceeds the allowed number of entries.');
+};
+
+/**
+ * When a new repeatable entry is created, check whether the entry number
+ * exceeds the value selected in the parent record's "visit type" choice field.
+ *
+ * Replace 'inspection' with your repeatable section's data name.
+ * Replace $visit_type with the data name of the choice field that
+ * controls the maximum number of entries allowed.
+ */
+ON('new-repeatable', 'inspection', () => {
+
+ // REPEATABLENUMBER() returns the 1-based index of the current entry.
+ // CHOICEVALUE($visit_type) returns the selected choice value as a string (e.g. "1", "2").
+ // If the entry number exceeds the allowed count, block it.
+ if (REPEATABLENUMBER() > parseInt(CHOICEVALUE($visit_type))) {
+ ALERT(
+ 'Too many entries',
+ 'You cannot add more entries than your current visit type allows.'
+ );
+
+ // Make all fields in this repeatable read-only to prevent data entry
+ FIELDNAMES('inspection').forEach((field) => {
+ SETREADONLY(field, true);
+ });
+
+ // Attach a validation listener to block saving this repeatable
+ ON('validate-repeatable', 'inspection', invalidateFunction);
+ }
+});
+
+/**
+ * When a repeatable entry is unloaded (user navigates away or closes it),
+ * reset any read-only state and remove the validation listener.
+ *
+ * This prevents the block from carrying over to other valid entries
+ * in the same session.
+ */
+ON('unload-repeatable', 'inspection', () => {
+
+ // Reset all fields in the repeatable back to editable
+ FIELDNAMES('inspection').forEach((field) => {
+ SETREADONLY(field, false);
+ });
+
+ // Detach the validation listener using OFF() to avoid memory leaks
+ // and unintended blocking of future valid entries
+ OFF('validate-repeatable', 'inspection', invalidateFunction);
+});
+```
+
+## Key concepts
+
+**`REPEATABLENUMBER()`** returns the 1-based position of the current repeatable entry being viewed or edited. If this is the 3rd entry, it returns `3`.
+
+**`CHOICEVALUE($field_name)`** returns the currently selected choice value as a string. Use `parseInt()` to compare it numerically.
+
+**`ON('new-repeatable', ...)`** fires when a user taps the "+" button to create a new repeatable entry — before any data has been entered.
+
+**`ON('validate-repeatable', ...)`** fires when the user tries to save the repeatable. Calling `INVALID()` inside this handler prevents it from being saved.
+
+**`OFF('validate-repeatable', ...)`** removes a previously attached event listener. Passing the named function reference (rather than an anonymous function) is required for `OFF()` to work correctly. This is essential for cleanup — without it, the validation block would carry over to other entries in the same session.
+
+## Customization
+
+- Replace `'inspection'` with the **data name** of your repeatable section.
+- Replace `$visit_type` with the **data name** of the choice field in the parent record that determines the maximum number of entries.
+- The choice field values should be numeric strings like `"1"`, `"2"`, `"3"` for this comparison to work.
From 98753930137b12732138355c17f8464d70d324bc Mon Sep 17 00:00:00 2001
From: Mike Meesseman <31196959+mmeesseman@users.noreply.github.com>
Date: Thu, 19 Mar 2026 11:38:51 -0400
Subject: [PATCH 03/11] Add documentation for displaying field value labels
Added documentation for helper functions to display human-readable field values in PDF reports, including usage examples for top-level fields and repeatable sections.
---
.../display-field-value-labels.md | 112 ++++++++++++++++++
1 file changed, 112 insertions(+)
create mode 100644 docs/REPORT BUILDER/reports-examples/display-field-value-labels.md
diff --git a/docs/REPORT BUILDER/reports-examples/display-field-value-labels.md b/docs/REPORT BUILDER/reports-examples/display-field-value-labels.md
new file mode 100644
index 00000000..c55af368
--- /dev/null
+++ b/docs/REPORT BUILDER/reports-examples/display-field-value-labels.md
@@ -0,0 +1,112 @@
+---
+title: Display field value labels
+excerpt: >-
+ Helper functions for displaying human-readable display values (labels) instead
+ of raw stored values in PDF reports. Includes support for top-level fields,
+ repeatables, ChoiceFields, AddressFields, and SignatureFields.
+deprecated: false
+hidden: false
+metadata:
+ title: ''
+ description: ''
+ robots: noindex
+next:
+ description: ''
+---
+
+# Display field value labels in PDF reports
+
+By default, Fulcrum stores and exposes the raw **stored value** of a field — for choice fields this is often a short code, and for complex fields like addresses and signatures it's a structured object. When building PDF reports, you typically want to display the human-readable **display value** (label) instead.
+
+These two helper functions make it easy to render display values for any field type, including inside repeatable sections.
+
+## Helper functions
+
+Add these functions to the top of your report's JavaScript section (the `
+
+
+
+