Creating a report
Five steps from a blank file to a working page. Steps 1–3 are required; skip 4 and you will have to type the URL, skip 5 and nothing breaks.
This builds StockPulse, the smallest of the shipped samples — so
if a step does not behave as described, compare against
sample/Inventory/models/reports/StockPulse.report.xml.
1. Write the definition
Section titled “1. Write the definition”Create models/reports/StockPulse.report.xml. Name and Slug are the only required attributes; a
report needs at least one dataset and one widget to be worth rendering.
<?xml version="1.0" encoding="utf-8"?><Report Name="StockPulse" Slug="stock-pulse" Label="Stock Pulse" Icon="fa fa-heart-pulse">
<DataSets> <DataSet Name="DsPulse" Mode="SingleRow"> <![CDATA[ SELECT COUNT(*) AS ActiveProducts, SUM(CASE WHEN p.Status = 'Draft' THEN 1 ELSE 0 END) AS DraftProducts FROM [Inventory].[Products] p WHERE p.IsDeleted = 0 AND p.IsActive = 1 AND p.CompanyId = @SessionCompanyId ]]> </DataSet> </DataSets>
<Widgets> <Tile Name="TlActiveProducts" Title="Active products" Icon="fa fa-box" Color="primary" Data="DsPulse" Value="ActiveProducts" Format="Integer"> <Detail Label="Still in draft" Value="DraftProducts" Format="Integer" Color="warning" /> </Tile> </Widgets>
<Layout> <Grid> <Row> <Column Span="4"><Item Name="TlActiveProducts" Width="12" /></Column> </Row> </Grid> </Layout>
</Report>Four things to get right, because each one is a hard parse error rather than a silent oddity:
- A
<Tile>must read aMode="SingleRow"dataset.MultiRowis the default, so a tile needs the attribute stated. (Charts and tables wantMultiRowand so say nothing.) - Every alias in a
SingleRowSELECT becomes a property a tile can map —ActiveProductsandDraftProductsabove. Any number of tiles can share one dataset, and that is one query between them. <Item Name>in the layout names a WIDGET, not a column. It must match a widget’sNameexactly.- The SQL must be a single plain
SELECT. No comments, no second statement, no writes — see the SELECT-only guard.
Scope every query with @SessionCompanyId as above unless you genuinely mean “all tenants”. Filters,
charts, tables and embedded views come next; the full vocabulary is in
Authoring a report.
2. Grant access
Section titled “2. Grant access”Miss this and the page returns 401 for everyone but a System user. A report is gated by an RBAC
resource named after its Name, in the Report category — whose defaults are exactly List, View,
Export.
In your *.rbac.xml, declare it:
<Resource Name="StockPulse" Category="Report" Description="Minimal product-count dashboard"/>…and grant it to the roles that should see it. The data endpoint needs List:
<Role Name="InventoryAdmin"> <Grant Resource="StockPulse" Capabilities="List,View"/></Role>An embedded <View> widget needs no extra grant here — it is gated by its own resource, which it
already has as a view.
3. Import it
Section titled “3. Import it”A report definition is a transfer document: it lives in the database, and importing is what puts it there.
- Open System → Automation → Reports.
- Click Import and select one or more
<Report>files. - Each is created, or updated in place, matched by its
Name.
A parse error surfaces here, against the file it came from, with a message naming the actual problem — so this is where an authoring mistake shows up, not at startup.
Re-importing an identical file reports Unchanged and adds nothing. A changed one appends a version, so the old definition stays recoverable. Export hands the stored text back byte-for-byte.
Or author it in the designer
Section titled “Or author it in the designer”The other route is the grid’s Add, which writes a minimal stub and opens the designer. There the loop is Draft → Open Preview → Publish: drafting stores your work without going live, Open Preview renders that draft in a new tab, and Publish is what visitors finally see. A report created this way reads Never published until you publish it once.
4. Put it on the navbar
Section titled “4. Put it on the navbar”The page is live at /reports/stock-pulse the moment step 3 succeeds — but nothing links to it yet. Add
an item to your *.navbar.xml:
<Section Label="Reports"> <Item Name="stock-pulse" Label="Stock Pulse" Icon="fa fa-heart-pulse" Route="/reports/stock-pulse" /></Section>The route is plural /reports/{slug}. The singular /report-view?report=… is the unrelated PDF
viewer, and /object/{name} is a grid — neither will resolve a report page.
Optionally add <ParentView Name="catalog" /> to the <Report> root, naming the navbar module it hangs
under, and the breadcrumb will show that module above the report.
Unlike the report itself, a navbar file is swept on startup, so this one needs a restart.
5. Point a dataset at another database (optional)
Section titled “5. Point a dataset at another database (optional)”By default a dataset runs against the app’s own database. To read somewhere else, name a source:
<DataSet Name="DsFlow" Source="InventoryReports"> … </DataSet>…and define it in the host’s appsettings.json:
"Genie": { "Sources": { "InventoryReports": { "Dialect": "SqlServer", "Connection": "SqlServer" } }}Connection is either a ConnectionStrings key or a literal connection string. Pointing it at a
read-only least-privilege login is worth doing — that, more than the SELECT-only guard, is what makes
a report unable to write. See Sources.
Checklist
Section titled “Checklist”| Symptom | Cause |
|---|---|
| Import fails with a message about a widget, dataset or item | An authoring error — the message names it. Fix and re-import. |
| Page loads but every card says “no data” | The SQL matched nothing for this tenant. Check @SessionCompanyId and your filter values. |
| 401 opening the page | No RBAC resource, or no List grant to the caller’s role (step 2). |
| Blank page / falls through to nothing | Wrong route — it is /reports/{slug}, plural (step 4). |
| “Choose your filters” instead of data | A Required filter is blank. That gates the page by design and executes no dataset. |
| A card is missing entirely | Its RolesAllowed excludes the caller, or it is Hidden="true". |
| A tile shows “—” | Its Value names no column in the dataset’s projection, or the dataset returned no row. |
| Designer changes are not on the page | They are in the draft. Click Publish — Draft deliberately does not go live. |
| Grid says Never published and Open is missing | The report has a stub document but no published version. Publish it once from the designer. |
| 401 on Open Preview, but the report itself loads | The draft preview needs Update on ReportDefinitions, not just View on the report. |
Where to go next
Section titled “Where to go next”- The report designer — the same document, built visually.
- Authoring a report — the complete element and attribute reference.
- Runtime & sources — how datasets execute, row caps, and row-level security.
- Report pages overview — versioning, permissions and the worked samples.