Wizara Develop Guide
Use EJS to turn submitted form data into live summaries, warnings, confirmation emails, printable layouts, review screens, scorecards, and clean human-readable output.
This guide uses proven Wizara patterns from real forms and exports. If a behavior is not confirmed, it is not included.
In HTML Blocks, emails, and templates, read values from locals.
<%= locals.companyName || 'Customer' %>
If a value must be saved, calculated, normalized, or prefilled, use Form Events.
setValue('service', 'Hotshot');
EJS is for display. Form Events are for mutation. This single rule prevents most Wizara issues.
Events write -> locals update -> EJS renders
| Wizara area | Use EJS for | Use something else when |
|---|---|---|
| HTML Block | Live summaries, warnings, instructions, review panels, printable sections. | You need to set a submitted field value. Use Form Events. |
| Confirmation Email | Readable submission summaries, conditional sections, uploaded file names, address formatting. | You need to change the form before submission. Use Events before the email renders. |
| Success Message Template | Thank-you messages that refer to submitted values. | You need a workflow action. Use connections, routing, or form settings. |
| PDF / printable output | Table-based layouts, paper-style forms, compliance packets, signed summaries. | You need modern web layout only. Email/PDF clients are more reliable with tables. |
This is the safest starting point for an HTML Block or email template. It handles empty values, arrays, dates, addresses, file uploads, and unknown objects without showing [object Object].
<%
const hiddenKeys = ['_errors', 'internalNotes', 'helperField'];
function hasValue(value) {
return value !== undefined && value !== null && value !== '';
}
function parseLocalDate(value) {
if (Object.prototype.toString.call(value) === '[object Date]' && !isNaN(value)) {
return value;
}
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value || ''));
if (match) return new Date(+match[1], +match[2] - 1, +match[3]);
const date = new Date(value);
return isNaN(date) ? null : date;
}
function formatDate(value) {
const date = parseLocalDate(value);
if (!date) return '';
return date.toLocaleDateString('en-CA', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
}
function formatAddress(value) {
if (!value || typeof value !== 'object') return '';
return [
value.address,
value.locality,
value.regionCode ? String(value.regionCode).replace(/^CA-/, '') : '',
value.postalCode
].filter(Boolean).join(', ');
}
function formatFile(value) {
if (!value || typeof value !== 'object') return '';
return value.name || value.fileName || value.filename || '';
}
function formatValue(value) {
if (!hasValue(value)) return '';
if (Array.isArray(value)) {
return value.map(formatValue).filter(Boolean).join(', ');
}
if (Object.prototype.toString.call(value) === '[object Date]') {
return formatDate(value);
}
if (typeof value === 'object') {
const address = formatAddress(value);
if (address) return address;
const file = formatFile(value);
if (file) return file;
return JSON.stringify(value);
}
if (value === true) return 'Yes';
if (value === false) return 'No';
return String(value);
}
function labelFromKey(key) {
return String(key)
.replace(/([a-z])([A-Z])/g, '$1 $2')
.replace(/_/g, ' ')
.replace(/\b\w/g, (char) => char.toUpperCase())
.replace(/\bBol\b/g, 'BOL')
.replace(/\bDob\b/g, 'DOB')
.replace(/\bVin\b/g, 'VIN')
.replace(/\bSin\b/g, 'SIN');
}
const rows = Object.entries(locals)
.filter(([key, value]) => !hiddenKeys.includes(key) && hasValue(value));
%>
Universal summary table
<table width="100%" cellpadding="8" cellspacing="0" style="border-collapse:collapse;">
<% for (const [key, value] of rows) { %>
<tr>
<td style="border-top:1px solid #ddd; width:35%;">
<strong><%= labelFromKey(key) %></strong>
</td>
<td style="border-top:1px solid #ddd;">
<%= formatValue(value) %>
</td>
</tr>
<% } %>
</table>
<p>Hello, <%= locals.firstName || 'there' %>.</p>
<% if (locals.message) { %>
<p><strong>Message:</strong> <%= locals.message %></p>
<% } %>
<%= locals["_16WheelerYears"] || '' %>
<% { const title = 'Driver Summary'; %>
<h3><%= title %></h3>
<% } %>
Use this near the end of a form to help the user double-check key answers before submitting.
<%
const name = locals.fullName || locals.name || 'Customer';
const company = locals.company || '';
const service = locals.service || locals.service_type || '';
const pickup = formatAddress(locals.pickupAddress);
const delivery = formatAddress(locals.deliveryAddress);
%>
<div style="border:1px solid #ddd; padding:16px; background:#f8fafc;">
<h3 style="margin-top:0;">Review Your Request</h3>
<p><strong>Name:</strong> <%= name %></p>
<% if (company) { %><p><strong>Company:</strong> <%= company %></p><% } %>
<% if (service) { %><p><strong>Service:</strong> <%= service %></p><% } %>
<% if (pickup) { %><p><strong>Pickup:</strong> <%= pickup %></p><% } %>
<% if (delivery) { %><p><strong>Delivery:</strong> <%= delivery %></p><% } %>
</div>
EJS can display a score. If the score must be submitted, calculate it in an On Field Value Change event and store it in a field.
HTML Block EJS<%
const p = Number(locals.probability) || 0;
const e = Number(locals.exposure) || 0;
const ih = Number(locals.impactHuman) || 0;
const ifi = Number(locals.impactFinancial) || 0;
const total = p + e + ih + ifi;
let rating = 'No Risk Rating';
if (total >= 10) rating = 'High';
else if (total >= 7) rating = 'Medium';
else if (total >= 4) rating = 'Low';
%>
<div style="padding:16px; border:1px solid #ddd;">
<h3 style="margin:0;"><%= rating %></h3>
<p style="font-size:32px; margin:8px 0 0;"><%= total %></p>
</div>
On Field Value Change if the score must be stored
async (fieldName, oldValue, newValue, values, setValues) => {
let p = toNumber(values.probability);
let e = toNumber(values.exposure);
let ih = toNumber(values.impactHuman);
let ifi = toNumber(values.impactFinancial);
switch (fieldName) {
case 'probability': p = toNumber(newValue); break;
case 'exposure': e = toNumber(newValue); break;
case 'impactHuman': ih = toNumber(newValue); break;
case 'impactFinancial': ifi = toNumber(newValue); break;
}
const total = p + e + ih + ifi;
setValues((prev) => ({
...prev,
riskRating: `${total}`
}));
function toNumber(value) {
const number = Number(value);
return isNaN(number) ? 0 : number;
}
};
values still contains the old value for the field that just changed. Use newValue for the active field or the total will be one step behind.
Use a local parser for YYYY-MM-DD values. This prevents dates from showing one day early or late.
<%
function parseLocalDate(value) {
if (Object.prototype.toString.call(value) === '[object Date]' && !isNaN(value)) {
return value;
}
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value || ''));
if (match) return new Date(+match[1], +match[2] - 1, +match[3]);
const date = new Date(value);
return isNaN(date) ? null : date;
}
function getOrdinal(day) {
const suffixes = ['th', 'st', 'nd', 'rd'];
const value = day % 100;
return day + (suffixes[(value - 20) % 10] || suffixes[value] || suffixes[0]);
}
function formatLongDate(value) {
const date = parseLocalDate(value);
if (!date) return '';
const weekday = date.toLocaleDateString('en-US', { weekday: 'long' });
const month = date.toLocaleDateString('en-US', { month: 'long' });
return `${weekday}, ${month} ${getOrdinal(date.getDate())}, ${date.getFullYear()}`;
}
const leaveDate = formatLongDate(locals.leaveDate);
const returnDate = formatLongDate(locals.returnDate);
%>
<% if (leaveDate && returnDate) { %>
<div style="padding:14px; background:#23333E; color:#fff; text-align:center;">
Away <strong><%= leaveDate %></strong> and back <strong><%= returnDate %></strong>.
</div>
<% } %>
EJS can show a helpful message, but it does not block submission by itself. Use this for clear guidance, and avoid duplicate error boxes by checking locals._errors.
<%
const email = typeof locals.email === 'string' ? locals.email.trim() : '';
const confirmEmail = typeof locals.confirmEmail === 'string' ? locals.confirmEmail.trim() : '';
const showEmailError =
!(locals._errors.email || []).length &&
!(locals._errors.confirmEmail || []).length &&
email &&
confirmEmail &&
email !== confirmEmail;
%>
<% if (showEmailError) { %>
<div class="WZ__Form__errors alert alert-danger">
<ul class="WZ__Form__errors-list">
<li class="WZ__Form__errors-item">Your email addresses do not match.</li>
</ul>
</div>
<% } %>
Use a Form Event to classify the request, write helper fields, then use EJS to show the notice. This keeps the display simple and the logic testable.
HTML Block EJS<%
const intent = locals.lookslikejobrequest || '';
const message = locals.jobRedirectMessage || '';
const url = locals.jobRedirectUrl || '';
%>
<% if ((intent === 'strong' || intent === 'possible') && url) { %>
<div style="padding:14px; border-left:4px solid #c21f32; background:#fff7f7;" role="status" aria-live="polite">
<p style="margin:0 0 8px;"><strong><%= message || 'This may belong in another form.' %></strong></p>
<a href="<%= url %>" target="_top">Open the correct form</a>
</div>
<% } %>
Use target="_top" when the link must escape an embedded iframe and replace the full browser page.
Email clients are stricter than normal web pages. Use tables, inline styles, hosted images, and simple conditionals.
<%
const name = locals.fullName || locals.name || 'there';
const service = locals.service || locals.service_type || '';
const message = locals.message || '';
%>
<table width="100%" cellpadding="0" cellspacing="0" style="font-family:Arial,sans-serif; color:#222;">
<tr>
<td style="padding:20px; background:#f8fafc;">
<h2 style="margin:0 0 8px;">Thanks, <%= name %>.</h2>
<p style="margin:0;">We received your submission.</p>
</td>
</tr>
<% if (service) { %>
<tr>
<td style="padding:14px 20px; border-top:1px solid #ddd;">
<strong>Service:</strong> <%= service %>
</td>
</tr>
<% } %>
<% if (message) { %>
<tr>
<td style="padding:14px 20px; border-top:1px solid #ddd;">
<strong>Message:</strong><br>
<%= message %>
</td>
</tr>
<% } %>
</table>
EJS can display a prefilled value, but it should not do the prefill. Use On Form Load with setValue, then EJS can read the result from locals.
async (setValue, setValues) => {
const allowedServices = ['FTL / Full Deck', 'Dry Bulk', 'Hotshot', 'Not sure'];
const applyValues = (data) => {
if (!data || typeof data !== 'object') return;
const service = String(data.service || '').trim();
if (allowedServices.includes(service)) {
setValue('service_type', service);
}
};
const params = new URLSearchParams(window.location.search);
applyValues({ service: params.get('service') });
window.addEventListener('message', (event) => {
const data = event.data && typeof event.data === 'object' ? event.data : null;
if (data?.wizaraPopulateQuote) applyValues(data);
});
};
HTML Block EJS
<% if (locals.service_type) { %>
<p>Selected service: <strong><%= locals.service_type %></strong></p>
<% } %>
EJS does not persist submitted data. If a value needs to be saved, use setValue or setValues in a Form Event.
Use On Form Load to read the URL or listen for parent-page postMessage. EJS can display the value after it exists.
Objects need deliberate formatting. Otherwise the user may see [object Object].
Field names, casing, option labels, and Unicode characters must match the current export.
| Symptom | Likely cause | Fix |
|---|---|---|
| EJS shows nothing. | The field name is wrong, empty, or uses different casing. | Export the current form and copy the exact field name. Use locals.fieldName. |
EJS shows [object Object]. |
An address, upload, ecommerce item, or object was printed raw. | Format objects with helper functions before printing. |
| A date is one day off. | new Date('YYYY-MM-DD') parsed the value as UTC. |
Use the local date parser from this guide. |
| A calculation is one step behind. | On Field Value Change used stale values. |
Replace the active field with newValue before calculating. |
| A range slider does not update. | The field expects a string. | Set the value as `${total}`. |
| A custom error appears twice. | Native validation and EJS both show an error. | Check locals._errors before rendering your custom message. |
| Prefill works on raw embed but not website page. | The query string is on the parent page, not inside the iframe. | Use parent-page postMessage plus On Form Load. |
| The email is messy or broken. | Layout is too web-like for email clients. | Use tables, inline styles, simple conditionals, and hosted images. |
| Output includes internal helper fields. | The template loops over all locals. |
Add a hiddenKeys list and skip internal fields. |
| A conditional check never matches. | The stored value is not exactly what the template expects. | Inspect the export or temporarily print the value during testing. |
If the form needs to show something, EJS is probably right. If the form needs to save, change, calculate, prefill, clear, or normalize something, use Form Events first and let EJS display the result.