The report designer
The designer at /report-designer builds a *.report.xml document visually: drag widgets onto a
12-column canvas, write each dataset’s SQL, edit every attribute in a property grid, and save. It is
the same document either way — the designer reads and writes the XML described in
Authoring a report, so a hand-authored file opens in it and a designed report
exports as a file you can commit.
Reach for it when you are building or adjusting a dashboard. Reach for the file when you want the report in version control alongside your other models, or when you are generating one from a script.
Getting there
Section titled “Getting there”Everything starts at System → Automation → Reports — the definitions grid:
| You want to | Do this |
|---|---|
| Create a report | Add. Give it a Name (Slug and Label are optional), and it opens in the designer ready to build. |
| Edit one | The row’s Design action. |
| Jump straight in | /report-designer?Id={slug}, or /report-designer for a blank document. |
Both endpoints are gated on the ReportDefinitions resource — View to open the designer,
Update to save. Deliberately not the report’s own resource: that one says who may read the
dashboard, and gating a save on it would make “can view” mean “can rewrite”, while leaving a brand-new
report (whose resource does not exist yet) ungated entirely.
The layout
Section titled “The layout”┌──────────────────────────────────────────────────────────────────┐│ Toolbox ▾ · ↶ ↷ · ▦ status · History · XML · Publish │├──────────────────────────────────────────────┬───────────────────┤│ CANVAS │ Properties ││ the 12-column layout, as a page │ Outline ││ │ Issues │├──────────────────────────────────────────────┤ ││ DATASETS (horizontal dock) │ │└──────────────────────────────────────────────┴───────────────────┘The page itself never scrolls. The canvas and the property pane are fixed and each scrolls internally, so the property pane and the dataset dock stay where you left them while the layout scrolls on its own.
Toolbar
Section titled “Toolbar”One row, in two halves. The left builds the canvas: the Toolbox dropdown (and the Placing … chip while a tool is picked up), Undo / Redo, and the 12-column guides toggle. The right acts on the document, in the order you reach for it: the search box, its status (the published version and whether this tab is unsaved, unpublished or published), then History, XML and Open Preview as icon buttons (hover for their names), then the save actions — Discard draft when there is one, Draft, and Publish last.
Search works like the wizard and workflow designers’ canvas search. Typing rings every matching widget, filter and dataset card and highlights the matched text — a widget matches on its name, title, kind, chart kind or the dataset it reads; a filter on its name, label or shape; a dataset on its name or source. Enter steps through the hits in reading order (filter bar, widgets, then the dataset dock), selecting each and scrolling the canvas to it; Shift+Enter steps back. The count beside the box reads 3 matches, then 1 / 3 as you step.
The report’s Name, Slug and Label are not repeated in the toolbar: the breadcrumb names the report, and the property grid edits it (select the report’s header on the canvas, or its row in the Outline). On a narrower screen the search box narrows first, and the row wraps only after that.
Toolbox (toolbar dropdown)
Section titled “Toolbox (toolbar dropdown)”The Toolbox button at the left of the toolbar opens a dropdown with every element the contract has, in four groups side by side — Widgets (Tile, the five chart kinds, DataTable, View), Layout (Grid, Row, Column, Section, Tab, Line), Filters (the six shapes), and Data (both dataset modes). The filter box at the top narrows the list and keeps its query between openings. It closes on Escape or a click outside it.
There are two ways to use an item, the same two the canvas offers for moving a placed node:
- Drag it onto a drop zone. Only the zones that can take it accept the drop.
- Click it to pick it up. The dropdown closes, a Placing … chip appears in the toolbar, and every zone that can take the item appears and reads Place … here; the rest stay out of the way. Click a lit zone to place it. Escape or the chip’s × puts it back.
Two kinds are placed the moment you click them, because there is no zone to click: a DataSet goes straight into the dataset dock, and a filter goes into the filter bar when the bar has no Column yet. Each container’s + still inserts the first thing it accepts (see below), so dragging is never the only path.
Canvas (centre)
Section titled “Canvas (centre)”Flow-laid-out, not a node graph — a report layout is a document, so the canvas reflows exactly as the
page will. Every container carries a visible tag (Grid, Row, Section · Replenishment), because
once rendered the difference between them is invisible and it is precisely what you are editing.
Columns show their span; sections and tabs collapse.
Drop zones appear only when you need them. A container that already holds something shows no drop zone while you are just looking — only while you are placing something (a picked-up or dragged node, or a toolbox item), and then only the zones that can take it. An empty container always shows its drop area, since that is its whole body, and the page’s own zone stays at the very bottom as the way to start another block. To add without the toolbox, use the container’s + chip, beside its move handle (on a Column, beside its span badge). It is labelled with what it adds — + Row on a Grid, + Column on a Row — inserts that at the end of the container, and selects it.
Widget cards are schematic: shape, slot names and binding, never sample data. Inventing numbers would suggest you are looking at your data when you are not — the report page is where a real render is reviewed, and Preview goes straight there.
A card’s colour — its stripe, icon and kind badge — is the colour of the toolbox item that created it:
a Tile card is the Tile tool’s colour, a Pie chart the Pie tool’s, a Grid container the Grid tool’s,
every filter shape amber. Both surfaces read one table, so what you drag is what you get. An authored
Color overrides it, which is the only way a card ever differs from its tool.
An <Item> whose Name matches no widget renders as an error card naming the miss, rather than as a
blank cell. That is the single most common report authoring mistake, and it is a hard parse error at
import — so the card carries its own × to delete it.
Moving what is already placed
Section titled “Moving what is already placed”Anything on the canvas can be moved: a widget card, a filter card, a Column, a Row, a whole Section. Drag it onto any drop zone, or use its move handle (the grip beside the ×) to pick it up and then click where it goes — dragging is never the only path here either, since HTML5 drag-and-drop cannot be driven from the keyboard.
While something is held, every zone that can take it appears and reads Move here; zones that cannot take it are not shown (an empty container’s is dimmed and out of the tab order), so the keyboard lands only on the places it can actually go. Escape puts it back down.
Three moves are never offered, because the document cannot hold them:
- Between the filter bar and the page layout. An
<Item>in the filter bar names a filter and one in the page layout names a widget, so a card carried across could never resolve. - Into a container that will not take it — a Column into a Grid, a Tile into a Row. The same rule the toolbox drop zones already enforce.
- Into itself or its own contents. Dropping a Section inside one of its own Columns would splice the branch out of the document and everything in it with it.
The canvas scrolls itself while you carry something. Hold the pointer near the top or bottom of the pane (or its sides, on a narrow screen) and it scrolls, faster the closer to the edge — so a target below the fold is reachable, which it otherwise would not be: a drag holds the pointer, and the wheel is unreliable mid-drag. This works for a node picked up with its handle as well as a dragged one, and for a new control dragged in or picked up from the toolbox.
A move is one undo step, and it moves the placement rather than copying it: a widget placed twice stays placed twice.
Every card’s × removes that placement — the <Item> — not the widget. A widget can be placed
more than once (the same <Item Name> in two Columns renders it twice from one query), so taking the
declaration away from one card would delete the other. Removing the declaration is Delete widget
in the property grid, which takes every <Item> for it with it. Both ask first, and say what goes
with it: which widgets a Column will unplace, which dataset stops being queried, whether a chart is
losing its only <Series>.
Dataset dock (bottom)
Section titled “Dataset dock (bottom)”Each <DataSet> as a card: its mode, its source, the column aliases scraped out of its SQL, and
which widgets read it. A dataset nothing reads is marked — it is never queried, which costs nothing but
is usually a leftover.
Selecting a card puts its SQL in the property pane, alongside the parameters available to it and a
reminder about the {{permission-filter}} placeholder. The dock collapses out of the way — its
chevron points the direction the click moves it.
Property grid (right)
Section titled “Property grid (right)”A two-column name/value grid rather than stacked fields — a report element carries a dozen-plus attributes. Required attributes are starred, closed vocabularies are real dropdowns, and a description pane below explains whichever property has focus.
That last part is load-bearing rather than decorative: report vocabularies are closed. An
unrecognised Kind, Format or Color fails the file rather than falling back to something
plausible, so the grid says what a value must be at the point where you type it.
Outline (right)
Section titled “Outline (right)”The Outline tab, between Properties and Issues, is the document as a tree — the root, the
filters, every layout node — plus an Unplaced widgets list, because a widget with no <Item>
never renders and its dataset is never queried.
The outline has its own filter box, separate from the toolbox’s. Typing narrows it to matching rows and their ancestors, so a match still shows where it lives rather than floating free of its Grid and Row.
Clicking an outline row selects it, scrolls the canvas to it and flashes it. The card can be most of a page away from the row, so the selection outline alone does not answer “which one did I just click”. The pane stays on the Outline so you can keep walking the tree; switch to Properties to edit what you selected. Clicking on the canvas does not scroll — re-centring on every click would yank the page out from under the pointer. Clicking an entry in Issues reveals it the same way, and opens its properties.
A third Issues tab sits beside them — see below.
XML and History are toolbar buttons that open dialogs, the same as in the workflow designer: both are whole-document concerns and a 336px pane is the wrong shape for either.
- XML shows the generated document, and it round-trips both ways. Paste a definition in, hit Load, and the designer rebuilds the canvas from it. Copy XML is in the same dialog.
- History lists every version, newest first, with the current one marked.
What the designer does for you
Section titled “What the designer does for you”Renames follow. Rename a widget and every <Item> that placed it is re-pointed; rename a dataset
and every widget that read it follows. Delete a widget and its <Item>s go with it. Without this,
the commonest edit in a report would leave the layout pointing at a name that no longer exists.
A Name derives the Slug until you set one yourself.
New elements arrive valid where they can. A new chart comes with one <Series> (a chart with none
fails the parser), a new dataset comes scoped with @SessionCompanyId, and dropping a widget into a
column creates both the widget and the <Item> that places it.
Column suggestions come from your SQL. Value, Category and a table column’s Name offer the
aliases scraped out of the bound dataset’s query, so you do not have to remember the projection. They
are suggestions — every one of those fields stays typeable, because an alias the scrape misses must
not become unauthorable.
Attribute order is canonical. Two saves of one document produce byte-identical text, which is what lets the engine recognise an unchanged save and skip appending a version.
Your comments survive. A hand-authored file’s <!-- … --> blocks come back out — the file header,
the section banners, the note on the dataset that carries {{permission-filter}} — with their text
and their own internal formatting untouched. A comment belongs to the element that follows it, which
is how it stays attached when the designer reorders or re-indents everything around it. The one shape
that does not survive is a comment trailing at the end of a block with no element after it: there is
nothing for it to belong to.
Whitespace is not preserved. The designer reformats to its own canonical layout — one attribute run per element, consistent indentation — so a round trip through it produces a diff even when nothing changed semantically.
A blank value deletes the attribute rather than writing Attr="". Those are not the same thing:
the parser reads an empty string where it expects “unset”, which changes what Required and Color
mean.
Ctrl+Z steps back through your edits one at a time; Ctrl+Shift+Z or Ctrl+Y replays them. The toolbar carries the same two controls, and each one’s tooltip names what it would reverse.
A run of keystrokes in one field is one step, not one per character — so undoing a rename gives you back the whole previous name. Moving to another field, selecting something else, or any click, drag or removal starts a fresh step. Loading a version from History and loading hand-edited XML are each a single step too, so a paste that rebuilt the canvas is one Ctrl+Z away.
Undo restores what was selected at the time as well as the document, so the property pane lands back on whatever you were working on.
Ctrl+Z inside a text box, the SQL editor or the XML dialog belongs to that editor — the designer keeps its hands off, because otherwise pressing it while typing SQL would silently discard your last layout change instead of a character.
The stack lives in the page. It covers everything back to the document you opened, and it is gone when you leave — it is not the same thing as the saved version History.
The Issues tab
Section titled “The Issues tab”The designer mirrors the parser’s own failures client-side, so an authoring mistake surfaces next to the thing that is wrong instead of as a rejected save. Clicking an issue selects what it names.
| Checked | Severity |
|---|---|
Name and Slug on the root |
error |
| A duplicate dataset, widget or filter name | error |
A widget’s Data naming no declared dataset |
error |
A Tile on a MultiRow dataset, or a chart/table on a SingleRow one |
error |
A missing required attribute (Value, Kind, Category, Object, a chart’s <Series>) |
error |
Pie/Donut with more than one series |
error |
A Color that is neither a palette key nor a hex; a Format outside the closed set |
error |
An <Item Name> matching no widget — with a “did you mean” for a near miss |
error |
SQL that is not a single, comment-free, read-only SELECT |
error |
| A dataset no widget reads | warning |
A widget with no <Item> in the layout |
warning |
| A row whose spans total more than 12 | warning |
Errors block Save; warnings do not. The server still parses on save — this is a head start, not a replacement, and it is deliberately a subset: duplicating the whole parser in TypeScript would create two vocabularies to keep in step, and the second one drifting is worse than a message arriving a round trip later.
Draft, Preview, Publish
Section titled “Draft, Preview, Publish”A live report keeps serving what it published while you rewrite it. Editing does not go live; the designer writes to the report’s draft, and only Publish moves that to what visitors see.
| Action | What it does |
|---|---|
| Draft | Stores the canvas as the report’s draft. Nothing published changes, no version is created, and no note is asked for — it is meant to be pressed often. |
| Open Preview | Renders the stored draft in a new tab, through endpoints only an author can reach. |
| Publish | Makes the draft what visitors see: appends a version, repoints the report, and clears the draft. This is where the note is recorded, because this is what creates a version. |
| Discard draft | Throws the draft away and reloads the published document. |
Three states, and the toolbar always shows exactly which one you are in:
| Pill | Meaning |
|---|---|
| unsaved | Changes are only in this browser tab. Click Draft. |
| unpublished | The draft is stored and ahead of what visitors see. Click Publish. |
| published | What visitors see matches this document. |
unsaved outranks unpublished, because work that exists only in one tab is the more urgent of the
two. Undo and redo keep working after a publish — publishing does not change the document, so every
step stays valid.
One report has one draft. Reopening the designer opens the draft if there is one, not the published version — otherwise your in-progress work would look like it had been lost. A draft is allowed not to parse, which is exactly the state you need to see and fix; Publish is where the parser has the last word, and it writes nothing if it refuses.
If someone else publishes while your draft is open, the draft is flagged stale — publishing it would discard whatever their save changed.
Open Preview
Section titled “Open Preview”A real link, so right-click, middle-click and Ctrl-click all behave the way they do anywhere else, and it opens in a new tab so the designer — and anything you have not stored — stays exactly where it was. It renders the stored draft, so click Draft first to include your latest changes; the link’s tooltip says so when the canvas is ahead.
The preview applies the same row-level filter the published report applies, resolved against the report’s own resource. It is a preview, not a privileged view: rows that would differ in production would make it useless.
Version history
Section titled “Version history”Every publish that changes the document appends a version; publishing a draft identical to what is live reports no changes, appends nothing, and still clears the draft. History re-reads the list each time it opens. Loading a version replaces the canvas but writes nothing until you draft and publish — and that publish appends a new version rather than rewriting history, which is why there is no separate restore.
Publishing can also rename the report: change Slug in the property grid, or Name in the
XML dialog (then Load) — Name is read-only in the grid, because renaming it renames the RBAC
resource and the old grants stop applying. Either way the publish updates that same report rather than
creating a second one. It is refused, with the clash named, if another
report already holds the name or slug.
Copy XML, in the XML dialog, puts the document on the clipboard for committing it next to your
other models. Export on the definitions grid hands the stored text back byte-for-byte.
Assistant-assisted authoring
Section titled “Assistant-assisted authoring”When the AI Assistant module is on (modules: { assistant: true }), the designer offers the report
to the assistant — attach it from the composer’s + menu (Current report) and three tools scoped
to this page become available. Opening the designer alone attaches nothing: until the chip is
there the assistant answers questions like it does anywhere else, which is what you want when the
question is about the data rather than the document. With it attached, ask in plain language and it
can:
- Read the report you are editing — the draft if there is one, otherwise the published version, read server-side under your own permissions. So “why is this dataset empty?” is answered against the real document, not a description of it.
- Preview a dataset — run one
<DataSet>and report the rows it actually returns, through the same path Open Preview uses. That turns “the SQL looks right” into “it returns nothing, because…”. - Propose a change — a rewritten document offered as an Apply button in the chat.
Clicking Apply loads the proposal onto the canvas as one undoable step: the Undo button’s tooltip reads “Undo Assistant change”, and Ctrl+Z reverses the whole thing. From there it is an ordinary unsaved edit — review it, then Draft and Publish as usual.
Three boundaries are worth being explicit about:
- The assistant never writes. Apply fills the canvas. Saving the draft and publishing remain your
actions, still gated on
UpdateofReportDefinitions— which each tool also re-checks for itself, so the chat is not a way around the designer’s own permission gate. - A proposal is validated before you see it. It runs through the same parser that gates publishing, so an Apply button only appears for a document that would publish. When the model gets it wrong, the parser’s message goes back to it — naming the bad reference and listing the real alternatives — and it corrects itself rather than handing you broken XML.
- Apply needs this page open. The handler is registered while the designer is mounted. Navigate away and the card tells you to come back rather than applying into nothing.
- “Reply in chat” means nothing is proposed. Add it to a message — or say not to change the report — and the turn is answered in the conversation instead, with figures and, where it helps, a chart. The propose-a-change tool is withheld for that turn rather than merely discouraged, so the document cannot move. See Answering in the chat.
If the designer has unsaved changes, save the draft before asking — the assistant reads the stored draft, the same document Publish would promote.
What it does not do
Section titled “What it does not do”- It does not run your SQL. Column suggestions are text-scraped from the query, not from executing it. Open Preview to see real data — or ask the assistant to preview a dataset for you.
- It does not create the RBAC resource. A report is gated by a resource named after its
Name, in theReportcategory — declare it in your*.rbac.xmland grantList, or the page returns 401 for everyone but a System user. See Grant access. - It cannot open a document it cannot parse. It shows you the stored text and the parse error instead, and lets you start from a blank canvas — because silently opening blank over a real report would replace it on the first publish. Note this applies to a draft too: a draft may hold text that does not parse, and the designer shows you that rather than pretending otherwise.
- A definition with no stored XML cannot be edited, only replaced. That is the
Source: nonestate the definitions grid shows, and it only arises for rows written before the XML was retained.
Where to go next
Section titled “Where to go next”- Creating a report — the same five steps, hand-authored.
- Authoring a report — the complete element and attribute reference.
- Runtime & sources — how the datasets you write actually execute.
- Report endpoints — including the designer’s four calls.