The Short Version

EJS reads form data

In HTML Blocks, emails, and templates, read values from locals.

<%= locals.companyName || 'Customer' %>

Events change form data

If a value must be saved, calculated, normalized, or prefilled, use Form Events.

setValue('service', 'Hotshot');

Do not mix them up

EJS is for display. Form Events are for mutation. This single rule prevents most Wizara issues.

Events write -> locals update -> EJS renders

Where EJS Is Used

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.

Copy/Paste Starter

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].

EJS starter helpers
<%
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>
Why this starter matters: real Wizara submissions often include arrays, address objects, upload objects, dates, booleans, and helper fields. A template that only prints strings will eventually break or show ugly output.

Core EJS Syntax

Print a value

<p>Hello, <%= locals.firstName || 'there' %>.</p>

Show a section only when a field has data

<% if (locals.message) { %>
  <p><strong>Message:</strong> <%= locals.message %></p>
<% } %>

Use bracket syntax for unusual field names

<%= locals["_16WheelerYears"] || '' %>

Avoid duplicate variable names

<% { const title = 'Driver Summary'; %>
  <h3><%= title %></h3>
<% } %>

Pattern: Review Card

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>

Pattern: Dynamic Risk or Score Card

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;
  }
};
Important: during On Field Value Change, 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.

Pattern: Date Confirmation Without Timezone Drift

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>
<% } %>

Pattern: Custom Validation Message

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>
<% } %>

Pattern: Job or Service Redirect Notice

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.

Pattern: Confirmation Email

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>
Email rule: avoid relying on flexbox in confirmation emails. Tables and inline styles are more dependable.

Pattern: Prefilled Form Values and EJS

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.

On Form Load
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>
<% } %>

What EJS Is Good For

  • Live review summaries
  • Risk and score displays
  • Conditional warnings
  • Confirmation email layouts
  • Printable compliance sections
  • Human-readable address and upload output
  • Dynamic thank-you copy
  • Showing helper-field results
  • Displaying prefilled service or campaign context
  • Hiding internal fields from emails
  • Turning field keys into readable labels
  • Checking native errors before showing extra guidance

What EJS Should Not Do

Do not use EJS to change values

EJS does not persist submitted data. If a value needs to be saved, use setValue or setValues in a Form Event.

Do not use EJS for URL prefill

Use On Form Load to read the URL or listen for parent-page postMessage. EJS can display the value after it exists.

Do not render unknown objects directly

Objects need deliberate formatting. Otherwise the user may see [object Object].

Do not guess field names

Field names, casing, option labels, and Unicode characters must match the current export.

Troubleshooting

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.

Testing Checklist

  1. Export the latest form JSON before writing EJS.
  2. Confirm exact field names and option labels.
  3. Test with blank values, single values, arrays, address objects, and uploaded files.
  4. Check dates that come from date fields, especially around month boundaries.
  5. Check the email output separately from the live form preview.
  6. If using an iframe prefill, test the raw Wizara embed first, then the published website page.
  7. Do not add styling until the data renders correctly.
  8. Keep one known-good export before every major revision.

Final Rule

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.