JS Query lets you write JavaScript to orchestrate data queries, transform results, and wire up UI logic without leaving the Designer. You can run JS code, call other queries, read component values, and return a structured result that the rest of the app can use.
When to use JS Query
- Transform data coming from a SQL/API query before binding it to a table or chart
- Orchestrate workflows (e.g., insert → refresh list → notify user)
- Conditional logic (e.g., validate inputs, branch by role, gate execution)
- Combine multiple queries (parallel or sequential)
- Lightweight utilities (formatting, mapping, grouping)
Anatomy
- Explorer (left)
- Lists your queries and components.
- Example:
jsQuery1
under Explorer.
- Editor (center)
- JavaScript editor area with Execute button.
- Placeholder shows typical data shaping, e.g.:
// type your code here // example: return formatDataAsArray(data).filter(row => row.quantity > 20)
- Response panel (bottom) previews the value you
return
.
- Settings (center, under editor)
- Run on page load: Executes automatically when the page mounts.
- Execute if: Runs only if the expression is truthy (guards execution).
- Event Handlers
- onSuccess: Actions to run when the JS Query completes without throwing.
- onFailure: Actions to run if code throws (or a promise rejects).
- Right Panel (component props)
- Where you bind component events (e.g., Button → onClick → run
jsQuery1
)
- Where you bind component events (e.g., Button → onClick → run
Execution Model
- A JS Query is asynchronous by default—use
await
freely. - Whatever you
return
becomes the query’s output and is available asjsQueryName.data
. - Throwing an error (or rejecting a promise) will trigger onFailure.
Example (sync):
return { ok: true, sum: 1 + 1 };
Example (async):
// Sequential orchestration
await getOrders.run();
const items = getOrders.data ?? [];
return items.filter(x => x.quantity > 20);
Referencing Other Queries
From within a JS Query, you can trigger other Data Queries or JS Queries:
// Run a query (e.g., SQL, REST) and read its output
await getCustomers.run({ country: countrySelect.value });
const rows = getCustomers.data;
// Optionally run more queries
await getOrders.run({ customerId: rows[0]?.Id });
// Return any serializable value
return { firstCustomer: rows[0], orders: getOrders.data };
Note: Use
.run(params?)
to execute a query programmatically. In the UI, the same query is executed using the Execute button or via component events (e.g., Button → onClick → Run query).
Reading Component Values
You can directly read component values by their names:
const name = nameInput.value;
const activeOnly = activeCheckbox.value;
const limit = Number(limitInput.value) || 50;
await searchUsers.run({ name, activeOnly, limit });
return searchUsers.data;
Typical properties you’ll use:
TextInput.value
,NumberInput.value
Select.value
,MultiSelect.values
Checkbox.value
,DatePicker.value
Table.selectedRow
,Table.selectedRow.data
,Table.selectedRows
If your app uses pages like
TransactionPage1
, component names are still globally addressable by their names (e.g.,Button1
,CustomerTable
).
Returning Data
- Always
return
the value you want to expose to bindings (tables, charts, etc.). - The Response panel shows the last returned value.
- Access it with
jsQuery1.data
elsewhere (bindings, expressions, other queries).
Shape the data:
await getOrders.run();
const rows = Array.isArray(getOrders.data) ? getOrders.data : [];
const top = rows
.filter(x => x.quantity > 20)
.map(({ id, product, quantity }) => ({ id, product, quantity }))
.sort((a, b) => b.quantity - a.quantity);
return top;
The editor hint shows
formatDataAsArray(...)
. When present, you can use it to normalize query outputs into arrays before filtering/mapping.
Settings
Run on page load
- Check this to execute once when the page loads.
- Useful for initial data fetches or bootstrapping cache/state.
Execute if (guard)
- Provide an expression; the query runs only if it evaluates to truthy.
- Example:
// Execute if: userSelect.value && userSelect.value !== 'all'
Event Handlers
onSuccess
- Chain post-success actions (e.g., refresh a table, open a modal, navigate).
- Prefer using onSuccess for side effects, and keep your JS Query focused on data logic.
onFailure
- Handle errors gracefully (e.g., set error text, show a message).
- Keep your JS code defensive (see error handling below).
Common Patterns
1) Validate → Run → Return
if (!nameInput.value) {
throw new Error("Name is required.");
}
await createCustomer.run({
name: nameInput.value,
surname: surnameInput.value,
balance: Number(balanceInput.value) || 0
});
// Optional: refresh list
await getCustomers.run();
return { created: true, count: (getCustomers.data || []).length };
2) Read–Transform–Bind
await getOrders.run({ start: startDate.value, end: endDate.value });
const rows = Array.isArray(getOrders.data) ? getOrders.data : [];
const byProduct = rows.reduce((acc, r) => {
acc[r.product] = (acc[r.product] || 0) + r.amount;
return acc;
}, {});
return Object.entries(byProduct).map(([product, total]) => ({ product, total }));
3) Parallel Queries
const [{ data: north }, { data: south }] = await Promise.all([
getSalesNorth.run(),
getSalesSouth.run()
]);
return { north, south };
4) Conditional Orchestration
await getUser.run({ id: userIdInput.value });
const user = getUser.data;
if (!user) throw new Error("User not found.");
if (user.status === "pending") {
await approveUser.run({ id: user.id });
await auditLog.run({ action: "approve", userId: user.id });
} else {
await archiveUser.run({ id: user.id });
}
return { status: "done" };
5) Pagination Helpers
const page = Number(pageNumber.value) || 1;
const size = Number(pageSize.value) || 20;
await getOrders.run({ offset: (page - 1) * size, limit: size });
return getOrders.data;
Error Handling
Use try/catch
if you want to keep the query “successful” but return an error payload; otherwise just throw
to trigger onFailure.
try {
await riskyQuery.run();
return { ok: true, data: riskyQuery.data };
} catch (e) {
// Option 1: let onFailure handle it
throw e;
// Option 2: swallow and return a structured error
// return { ok: false, message: e.message };
}
Best practice
- Validate incoming inputs early
- Fail fast on missing/invalid values
- Keep return shapes consistent (e.g., always
{ ok, data, message }
)
Performance Tips
- Do less per query: one responsibility (fetch OR transform OR orchestrate)
- Use Execute if to avoid wasted calls
- Cache in a page-level store or reuse existing query results where possible
- Parallelize independent reads with
Promise.all
- Debounce input-driven calls from the UI (e.g., search) via button or a dedicated debounce in JS
Security & Safety
- Never embed secrets in JS Queries—use secure Data Sources and server-side credentials.
- Validate/escape user inputs if you pass them to SQL/API queries.
- Avoid returning sensitive data unless explicitly needed by the UI.
Binding Examples (outside the JS Query)
- Table data:
{{ jsQuery1.data }}
- Chart series:
{{ jsQuery1.data.map(x => x.total) }}
- Conditional UI:
{{ !!jsQuery1.data && jsQuery1.data.length > 0 }}
Trigger from a Button:
- Select the Button → onClick → Run query → choose
jsQuery1
.
Chain with onSuccess:
- In
jsQuery1
→ onSuccess: Run query →getCustomers
(to refresh list) - In
jsQuery1
→ onFailure: Set text of errorLabel or open a small error dialog
FAQ
Q: Where do I see what my query returned?
A: In the Response panel under the editor. You can also bind jsQueryName.data
anywhere.
Q: Can I call multiple queries from one JS Query?
A: Yes—sequentially with await
, or in parallel with Promise.all
.
Q: How do I prevent it from running automatically?
A: Keep Run on page load unchecked, and bind execution to a button or a specific event.
Q: What happens if I don’t return anything?
A: jsQueryName.data
will be undefined
. Always return
a value if you plan to bind the result.
Copy-paste Starters
Fetch & return
await getCustomers.run();
return getCustomers.data;
Input-driven fetch
await search.run({ term: searchInput.value?.trim() || "" });
return search.data;
Insert → Refresh → Return count
await createItem.run({ name: nameInput.value });
await listItems.run();
return { ok: true, count: (listItems.data || []).length };
Filter and format
await getOrders.run();
const rows = formatDataAsArray(getOrders.data);
return rows.filter(r => r.quantity > 20).map(({ id, product, quantity }) => ({ id, product, quantity }));