HTML Reports

Last updated 25 May 2026

A Flexie HTML report template rendering as a custom KPI dashboard page with cards and a chart

When you choose HTML as the output format, the report's body is not a SQL query. It is a template you write yourself, in HTML, with CSS for styling, JavaScript for interactivity, and Flexie Scripting tokens to pull and shape live data. Whatever you write is what the report shows.

Start at Reports (/reports), click New, set output format to HTML.

When to use it

HTML reports are the right tool for anything the data grid can't present:

Combined with dashboard widgets, HTML reports make almost any widget you can imagine possible: drop the HTML report in as a widget and the dashboard renders your custom layout.

The report settings

Same as for Data Grid reports, except:

What you can write

Anything that renders in a browser:

A minimal example:

<style>
  .card { padding: 1rem; border: 1px solid #eee; border-radius: 8px;
          background: #fff; margin-bottom: .75rem; }
  .num  { font-size: 2rem; font-weight: 600; color: #1a73e8; }
  .lbl  { font-size: .85rem; color: #555; text-transform: uppercase; }
</style>

<h2>This week at a glance</h2>

<div class="card">
  <div class="num">{{ findCount("deal", "is_won", 1) }}</div>
  <div class="lbl">Deals won (all-time)</div>
</div>

<div class="card">
  <div class="num">{{ findCount("case", "status", "open") }}</div>
  <div class="lbl">Open cases</div>
</div>

Open the report and you get two KPI cards backed by live data. The same idea scales up to a full custom page.

Pull data, then weave it together

Because Flexie Scripting is available inside the template, you can run multiple lookups and combine them however you like:

{% set winners = query("
  SELECT u.full_name, SUM(d.amount) AS total
  FROM deals d
  JOIN users u ON u.id = d.owner_id
  WHERE d.is_won = 1
  GROUP BY u.id
  ORDER BY total DESC
  LIMIT 10
") %}

<h2>Top closers</h2>
<ol>
  {% for row in winners %}
    <li>
      <strong>{{ row.full_name }}</strong> —
      {{ row.total | number_format(0) }}
    </li>
  {% endfor %}
</ol>

Or fetch a list of records and render them as cards, a table, a kanban: your choice.

Loops, conditions, formatting

Everything you would expect from Flexie Scripting basics works:

{% set deals = query("
  SELECT id, name, amount, is_won, is_lost
  FROM deals
  WHERE owner_id = {user_id}
  ORDER BY date_modified DESC
  LIMIT 50
") %}

<table>
  <thead><tr><th>Deal</th><th>Amount</th><th>Status</th></tr></thead>
  <tbody>
    {% for d in deals %}
      {% set status = d.is_won ? "Won" : (d.is_lost ? "Lost" : "Open") %}
      {% set colour = d.is_won ? "#0a8a3f" : (d.is_lost ? "#c32c2c" : "#888") %}
      <tr>
        <td>{{ d.name }}</td>
        <td>{{ d.amount | number_format(2) }}</td>
        <td style="color: {{ colour }}">{{ status }}</td>
      </tr>
    {% endfor %}
  </tbody>
</table>

The same placeholder variables that work in data grid queries ({user_id}, {group_id}, {role_id}, {timezone}) work inside query(...) calls within HTML reports.

What the template can use

The full Flexie Scripting toolkit is available in HTML reports:

In short, everything a normal Flexie Scripting context can do is available, including the security helpers (signUrl, jwtEncode, hashHmac) for embedded actions and links.

Restrictions still apply

HTML reports are powerful, but the underlying data lookups still play by the rules:

JavaScript and CSS, what runs and where

JavaScript and CSS in your template run in the viewer's browser like any other HTML page. There is no sandboxing on the rendered output. That means:

A common pattern, emit JSON, render with JS

<div id="chart"></div>

<script>
  const data = {{ query("
    SELECT month, sales
    FROM monthly_sales
    WHERE year = 2026
    ORDER BY month
  ") | json_encode | raw }};

  data.forEach(row => {
    const bar = document.createElement("div");
    bar.style.width = row.sales / 1000 + "px";
    bar.textContent = row.month + ": " + row.sales;
    document.getElementById("chart").appendChild(bar);
  });
</script>

The | json_encode | raw chain turns the query result into a JSON literal embedded straight in the script.

Use | json_encode whenever you embed a value inside a <script> tag. It quotes and escapes the data so a stray quote or newline in the data can't break your JavaScript or open an injection.

Filters in HTML reports

Filters can still be defined on an HTML report, using the same JSON shape covered in Report filters (in depth). Their submitted values land in the user's session, and your template reads them however you like.

In practice, HTML reports often use Flexie Scripting tokens directly inside their query(...) calls (via the same placeholders, {user_id}, {group_id}) rather than the {filters} substitution that data grid reports use. The richer rendering means you can also read filter values from the URL query string with getQueryString("name").

Security, what to keep in mind

Anyone who can edit an HTML report can place arbitrary HTML, CSS, and JavaScript in the rendered page. Anyone who can view the report runs that code in their browser. So:

Next steps