Writing Templates
Complete reference for the QueryTemplate JSON schema — every field, its type, default, and behaviour.
Root Template Fields
The root object is the entry point of every template. It is identified by "isTemplate": true and is the only record directly addressable by the API via its catalogue code.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name |
string | Yes | — | Human-readable name for the template. Used in admin UIs and diagnostic logs. Does not affect execution. |
sql |
string | Yes | — | The SQL SELECT statement executed for the root level. Must filter by TenantID = @TenantID. Supports @ParameterName tokens declared in the parameters array. |
recordType |
string | Yes | — | A label identifying the type of record returned (e.g., "Department", "PayrollRecord"). Appears in HTML output headers and diagnostic traces. |
isTemplate |
boolean | Yes | — | Must be true on the root object. Marks this record as the API-addressable entry point. All child and grandchild records must set this to false. |
parameters |
array | No | [] |
Caller-supplied parameters (excluding TenantID, which is injected automatically). Each entry is a Parameter Object — see below. |
options |
object | No | See Options | Controls output format, caching, and recursion depth. See the Options Object table below. |
children |
array | No | [] |
Zero or more ChildCollection objects defining the next level of the hierarchy. Each child is executed once per root row, with @parent tokens resolved to the current root row's values. |
Parameter Object
Each entry in the parameters array declares one caller-supplied SQL parameter. Parameters are resolved from the API query string, the POST body, or the EvaluationContext (for expression directives).
| Field | Type | Description |
|---|---|---|
name |
string | Parameter name without the @ prefix (e.g., "DepartmentID"). Must match the token used in the SQL string. |
type |
string | SQL type for the parameter. Accepted values: int, nvarchar, bit, datetime. The engine uses this to cast the incoming string value before binding it to the SQL command. |
defaultValue |
any / null | Value used when the parameter is not supplied by the caller. Set to null for required parameters — the engine will return an error if a null-default parameter is missing at runtime. |
Options Object
The options object controls how the engine formats and caches results. All fields are optional; the defaults shown below are applied when the field is absent.
| Field | Type | Default | Description |
|---|---|---|---|
outputFormat |
json | html |
json |
Controls the response format. json returns a compact JsonArray tree. html returns an expandable cascaded HTML table. Can be overridden at call time via the endpoint suffix (.json / .html) or via pipe options in the expression directive. |
outputFormatTemplate |
fragment | standalone |
fragment |
Applies when outputFormat is html. fragment returns a bare table suitable for embedding in an existing page. standalone wraps the table in a complete <html> document with inline styles — suitable for email bodies or saved files. |
maxDepth |
int | 5 |
Maximum recursion depth the engine will traverse. Prevents runaway queries on self-referencing hierarchies. Set lower (e.g., 3) for shallow trees to improve clarity. |
cacheSeconds |
int | 0 |
Duration in seconds to cache the result for identical parameter combinations. 0 disables caching (always fresh). Use 60–300 for read-heavy, infrequently-changing data. Always use 0 for audit and compliance queries. |
ChildCollection Object
Each entry in the children array defines one nested collection. The engine executes the child SQL once for every row returned by the parent query, substituting @parent.* tokens with the current parent row's column values.
| Field | Type | Description |
|---|---|---|
collectionName |
string | The key used in the JSON output (or the section label in HTML output) for this child array. Use camelCase (e.g., "employees", "auditEvents"). |
sql |
string | The SQL SELECT statement for this child level. Must reference @parent.ColumnName to join to the parent row and must filter by TenantID = @TenantID. |
recordType |
string | Label for records at this level. Used in HTML headers and diagnostic output. |
isTemplate |
boolean | Always false for children. Child records are not addressable directly via the API — they are always fetched as part of a root template execution. |
children |
array | Recursive. Zero or more ChildCollection objects defining the next level beneath this one. Nesting is limited by maxDepth in the root options. |
SQL Writing Guidelines
- Always filter by TenantID. Every SELECT at every level must include
AND TenantID = @TenantID. The engine injects the tenant from the caller's JWT, but SQL must enforce it to prevent cross-tenant data leakage if the engine layer is ever bypassed. - Reference the parent via
@parent.ColumnName. Child SQL joins to the parent row using the@parent.prefix. The column name must exactly match the column returned in the parent SELECT (case-insensitive). For grandchildren, use@parent.parent.ColumnName. - Use parameterized queries only. All dynamic values must come from declared parameters or
@parenttokens. Never concatenate user-supplied strings into the SQL. The engine usesSqlCommand.Parametersfor all bindings. - Avoid ORDER BY on parent-level queries. Because the engine issues one child query per parent row, the order of parent rows affects execution order but not result structure. Apply ORDER BY at child levels where sort order is meaningful for display.
- Filter soft-deleted records. Always include
AND Deleted = 0 AND Archived = 0unless the use case explicitly requires deleted records (e.g., an audit trail). Omitting these filters returns logically deleted rows to callers. - Qualify all column names. In queries that join multiple tables, prefix every column with its table alias to prevent ambiguity errors when schema changes add identically-named columns to joined tables.
Always list columns explicitly in production templates. SELECT * breaks in two ways: (1) schema changes — a new column added to the underlying table is silently returned to all callers, potentially exposing sensitive data; (2) performance — unnecessary columns are transmitted, serialized, and cached, increasing latency and storage cost. List only the columns your consumers actually need.
Full Example — 3-Level Template
A realistic three-level template: Clients (root) → Projects (child) → Timesheets (grandchild). Each level uses explicit column lists, TenantID filtering, and @parent token joins.
{
"name": "Client Project Timesheets",
"sql": "SELECT c.ClientID,
c.ClientName,
c.IndustryCode,
c.AccountManagerID,
c.ContractStartDate,
c.ContractEndDate
FROM Clients c
WHERE c.TenantID = @TenantID
AND c.IsActive = 1
AND c.Deleted = 0
AND c.Archived = 0
ORDER BY c.ClientName",
"recordType": "Client",
"isTemplate": true,
"parameters": [
{ "name": "PeriodStart", "type": "datetime", "defaultValue": null },
{ "name": "PeriodEnd", "type": "datetime", "defaultValue": null }
],
"options": {
"outputFormat": "json",
"outputFormatTemplate": "fragment",
"maxDepth": 5,
"cacheSeconds": 120
},
"children": [
{
"collectionName": "projects",
"sql": "SELECT p.ProjectID,
p.ClientID,
p.ProjectName,
p.ProjectCode,
p.Status,
p.BudgetHours,
p.BillableRate
FROM Projects p
WHERE p.ClientID = @parent.ClientID
AND p.TenantID = @TenantID
AND p.Deleted = 0
ORDER BY p.ProjectName",
"recordType": "Project",
"isTemplate": false,
"children": [
{
"collectionName": "timesheets",
"sql": "SELECT ts.TimesheetID,
ts.ProjectID,
ts.EmployeeID,
ts.WorkDate,
ts.HoursLogged,
ts.IsBillable,
ts.NarrativeNote,
ts.ApprovedBy,
ts.ApprovedOn
FROM Timesheets ts
WHERE ts.ProjectID = @parent.ProjectID
AND ts.TenantID = @TenantID
AND ts.WorkDate >= @PeriodStart
AND ts.WorkDate <= @PeriodEnd
AND ts.Deleted = 0
ORDER BY ts.WorkDate DESC",
"recordType": "Timesheet",
"isTemplate": false,
"children": []
}
]
}
]
}
Store this with catalogue code MQ_CLIENT_PROJECT_TIMESHEETS and call it as:
GET /api/v1/expressions/multiquery/MQ_CLIENT_PROJECT_TIMESHEETS.json
?PeriodStart=2025-11-01&PeriodEnd=2025-11-30
Authorization: Bearer <token>