Skip to main content

Design a Report

Goal​

Lay a report out inside the platform — on a canvas, beside the queries that fill it — instead of drawing it in a separate tool and pasting JSON back in.

Where it opens​

Open your solution, choose Reports, select a report, and press Design. The canvas opens in place, under the same breadcrumb and the same environment and solution pickers you were already working in. It is not a second application and not a new tab: designing a report is maker work, and this is the maker portal.

Design and Edit are two commands because they own two different halves:

  • Design is the layout — the page, its bands, its tables, its logo.
  • Edit is everything a canvas cannot know about — which saved queries fill the template, and which field narrows each of them when somebody prints one record.

You will use both on most reports, and neither overwrites the other's half.

Prerequisites​

  • You are signed in as a maker, with a solution and an environment selected.
  • The report already exists. Create it first — see Create a report — then design it.

Steps​

  1. Open your solution and choose Reports.
  2. Select the report and press Design.
  3. If the report has never been designed, the canvas opens with only the Styles board on it — the colours and text styles a design starts from, which never print. Use Insert → Report page to drop a page, and lay your content out inside it.
  4. Draw the report: bands for the header and footer, a repeater for the rows, text bound to your dataset's fields. The palette and the layers panel work as they always have; the inspector now has two tabs — see below.
  5. Press Save.

Start from a sample​

Insert → Sample reports drops a finished report page onto the canvas beside whatever you have already drawn — nothing is replaced — and loads its sample data into the report data editor, so it previews immediately.

The list has two halves, and they answer different questions.

The first five are finished documents — Invoice, Sales, Contact list, Summary, Dashboard — and they are there to show what a report can look like.

The next four each demonstrate one feature, which is what you want when you know the effect you are after and not the gesture. Each one carries a line in its own page footer saying where to look:

SampleShows
Styling (zebra + conditional format)Banded rows, a colour and weight that change above a threshold, a status colour, and a flag that appears only on the rows it applies to
WatermarkA DRAFT stamp over every page, and a document-control block in the corner
Repeating header & footerA letterhead and a footer on every sheet, column headings that come back wherever the table continues, and a per-sheet subtotal beside a whole-table total
ChartsA bar chart and a line chart, and a table reading the same dataset underneath

Open one, select a band or a cell, and read its expressions in the inspector or on the Code page. They are ordinary drawings — edit them, keep the half you wanted, delete the rest.

These four bind a real table

Their cells read row.par_name, row.par_amount, row.par_currency and row.par_status — the columns of the sample expenses table — rather than invented names. That is what lets the same drawing be installed as a working report with real rows behind it, instead of previewing nicely and printing blank. Re-point them at your own table by editing the dataset and the bindings; the techniques are unchanged.

Copy the row-banding rule

Zebra striping is Background on the repeater's row frame, and it reads rowIndex % 2 == 1 ? '#f8fafc' : '' — the row number, not a field. Reading a value looks identical until somebody re-sorts the table.

A chained test needs brackets

a ? b : c ? d : e does not parse. Write row.status == 'Approved' ? '#059669' : (row.status == 'Rejected' ? '#be123c' : '#b45309'). An expression that cannot be parsed leaves the property exactly as you drew it, so the symptom is a colour that never changes rather than an error.

Generate with AI​

Generate with AI describes a page in words and draws it. Describe what you want — the box is large on purpose, a page worth generating takes a paragraph — and press Generate, or Ctrl/⌘+Enter. What comes back is added to what is already on the canvas; nothing you drew is replaced. For a fresh start, clear the canvas first, then generate.

Whatever comes back is checked before anything appears — every shape has to be one this editor can actually build — so a reply naming a shape that does not exist does not put a broken box on your page.

It corrects itself before it gives up. When nothing usable comes back, the platform sends the reply straight back to the model with the problem named, rather than asking you to fix an error you never wrote. It tries up to three times, and the button reads Correcting… attempt 2 of 3 while it does.

A partly usable reply is kept, not thrown away. The shapes that could be drawn are on the canvas and the dialog stays open listing what was left out — so a reply of ten shapes where two were unusable arrives as eight shapes and two sentences, rather than as eight shapes and silence. A partial result you are not told about is one you read as your own description having asked for less.

A generated report is plain unless you ask for colour: dark text on white, a light grey header bar for a table, square corners. Say "in our brand colours" or "with a coloured header" to get more.

Add the dataset first. The AI is told this design's datasets and the fields each row has, so a report generated after you add a dataset repeats over it and shows its real fields. Generated before, it has to guess a dataset's name, and the dialog says which one it guessed.

You describe the design, and nothing else. Which AI model answers is set up once for the whole platform by an administrator — there is no key to paste and no provider to pick.

If there is no AI button, this platform has no AI set up. Ask an administrator; it is a setting on the server, not something a maker can turn on.

The dialog says what the AI will be told before you ask. It names the datasets the AI is told about, by the names you gave them (items or expenses, either works), or warns that the report has none yet. Add the dataset first: without one the AI guesses a name and the report previews empty.

Changed a dataset's view? Come back to the designer. When you add a column to the view in another tab, the designer reads the view again as you return, so the new column is offered for binding, reaches the AI and appears in the preview. A total over a column the view does not select prints blank, so add the column to the view, not only to the report.

The inspector's two tabs​

The properties panel on the right has two tabs, and they answer two different questions:

  • Graphic — what the object is: where it sits, how big it is, its fill, its stroke, its text.
  • Report — what the object means to the report: that this frame is the page, or a header band, or a repeater, and what a text or an image is bound to.

A dot on the Report tab means the selected object is already part of the report — the page, a band, a bound text — so you can see it without opening the tab. Whichever tab you leave open stays open as you select other objects.

Some objects have nothing to put on the Report tab: only a frame can be the page or a band, and only a text or an image can be bound to data. The tab says so rather than opening empty.

Saving does two things at once, and this is the part worth understanding: it compiles what you have drawn into the template that prints, and it stores your drawing alongside it. That is why you can press Design again tomorrow and get your layout back rather than a reconstruction of it — the drawing is kept, not just its output.

One page design, however many sheets it prints​

A page design paginates: the body flows onto as many sheets as the content needs, and the page header and page footer are redrawn at the top and bottom of each. A hundred-row invoice list is one page design and four printed pages.

Several page designs in one report — sections​

Draw a second Report page frame on the canvas and you get a section: a cover laid out one way, the body another, terms and conditions on the back. Every frame tagged as a report page is one section, in the order they sit on the canvas, and each section starts on its own sheet.

Each section has its own margins and its own page header, page footer, letterhead and watermark — a cover with no header and no page number, and a body that has both, in one report. A section with nothing in it prints nothing at all rather than a blank sheet.

The page numbers count the whole report, not the section: the cover is page 1 of n, where n counts every sheet the body and the terms take too. Somebody holding a printed page wants "3 of 9".

note

Every section prints on the same size of paper, taken from the first page frame. A printer has one tray, so a report that mixed A4 and A5 would print correctly nowhere. Resize one of the frames and the designer says so when you save, then prints everything on the first frame's paper. Margins are yours to change per section; the sheet is not.

A section is "from here, the page looks different". When you only want a fresh sheet with the same design, that is the mark below.

Starting a region on a new page​

Pagination breaks a page when the next block will not fit. That answers "do not split this row"; it cannot answer "this part starts on its own page", because that has nothing to do with how much room is left.

Select the region — a band, a repeater, a columns container, a chart — and tick Starts on a new page on the inspector's Report tab. The report breaks immediately before it, full page or not. Use it for a run of claims per customer, or wherever a fresh sheet is wanted with the same page design. When the page itself should look different, draw a second page frame and use a section instead.

Two details worth knowing:

  • It never leaves a blank sheet. A break at the top of a page that is already empty does nothing, so ticking it on the report's first region — or on two regions in a row — costs you no paper.
  • On a band that repeats as a table header, the break lands before the header, which is what you want: the new page opens with the column headings and the rows follow.

Do not reach for it to make a band fall neatly — padding a band with empty space until it overflows looks right today and breaks the moment the data changes. The tick is the durable way to say it.

Repeating over a dataset​

A repeater frame draws its contents once per row. Tag the frame as a repeater on the inspector's Report tab, then choose what it walks in Repeat over.

Repeat over is a dropdown of this report's own datasets — rows, invoices — so there is no name to remember and none to spell wrong. If the one you want is not on the list, add it under Edit in the Reports drawer and it appears here.

A list rather than a box you type in, on purpose: a repeater walks a whole dataset, so "which one" has exactly as many answers as the report has datasets. Narrowing the rows — only the approved claims, say — belongs in the dataset itself, where the query lives and where every band reading that dataset gets the same answer; put it in one frame's box and the next band needs its own copy of it.

What a text or an image shows​

A text or an image inside a repeater is bound, and the binding is written in the same editor as every other piece of BS Lang in the platform: it completes what you can call, it shows what a function takes, and it underlines a mistake as you type rather than printing a blank cell later.

On the inspector's Report tab, Binding is a dropdown of the fields you can bind to, because that is what a binding usually is and a name you can pick is a name you cannot misspell.

Anything more than a field is written on the Code page: select the text and press Write code in the toolbar (the </> button). The list down its side starts with What it shows, the binding, and goes on to every property that can follow the data (see Style that comes from the data, below). The editor has room to work in: completion, what each function takes, and a red underline under a mistake as you type, and the list marks each entry Written or Has an error. That is where concat('Page ', pageNumber, ' of ', pageCount) goes. The ✕ in its corner takes you back to the drawing, and what you wrote is stored with it when you press Save.

Open the eye to see what you can write. Every expression box in the platform carries one, and behind it is everything this expression can reach: Page (pageNumber, pageCount, page.rows), Repeater row (row, rowIndex, rows, rowCount), the columns of the dataset this repeater walks, the report's own aggregates with what each one takes, and the language's own functions. Search it, click an entry, and it is inserted where your cursor is. It is there because completion cannot answer "what is in this row?" — completion tells you what comes next once you have started typing, and the eye tells you what is there before you have.

Inside a repeater the columns are written as row.<field> — row is a record of exactly one dataset, and a field from another would resolve to nothing. Outside a repeater there is no row at all, so the same fields are addressed through their dataset: rows[0].total.

A column from a related table is written under the name the query gave the join. A saved query that joins the customer as customer offers its columns as row.customer.par_name and row.customer.par_credit_limit, and the list functions take the same name in quotes: sum(rows, 'customer.par_credit_limit'). Rename the join in the query and the bindings have to follow it.

And it is an expression, not only a field name. row.amount * 1.2, row.status == 'Approved' ? 'Yes' : 'No', concat(row.first, ' ', row.last), sum(page.rows, 'amount') for a per-page total — the same language as a Business Rule, a screen's bindings and a solution script.

Where you areWhat is in scope
A row of a repeaterrow, rowIndex, rows, rowCount, and every dataset by name
A page header or footerpageNumber, pageCount, page.rows, page.rowCount
Anywherethe datasets by name, data, and the aggregates below

Totals, groups and running sums​

A table total goes in a total row. Draw a band under the repeater and tick Total row for the repeater above on the Report tab. It is drawn once after the last row and it sees rows and rowCount for the whole dataset — so sum(rows, 'amount') there is the table's total.

That is a different number from a page total, which is sum(page.rows, 'amount') in the page footer and counts only the rows that landed on that sheet. Both are useful; pick deliberately.

A running total is sum(take(rows, rowIndex + 1), 'amount') in the row itself — every row up to and including this one.

Grouping is the dataset's job. Save a query that groups — one row per status, with a count and a total (see Save a query) — add it to the report as a second dataset under Datasets, and repeat over it like any other: each row it draws is one group, already counted and totalled, and a total row under it adds the groups up. A repeater cannot sit inside another repeater's row, so a report does not list each group's own rows under a heading for the group.

Reaching a second dataset is lookup(regions, 'code', row.region).manager — the first row whose column matches, so you can print a name your own query did not fetch.

Style that comes from the data​

Select a band, a cell or a shape, press Write code, and pick a property from the list under Properties: each takes an expression, answered per row:

PropertyExample
Backgroundrow.amount > 1000 ? '#fee2e2' : ''
Text colourrow.overdue ? '#b91c1c' : ''
Font weightrow.isTotal ? '700' : ''
Shown whenrow.note != ''

Empty means "leave it as I drew it" — that is why the examples have a bare '' on the other side instead of repeating your own colour, and why switching this on can never blank a report that was fine. Shown when is the exception: a false answer removes the element completely, background and border included, which is what binding text to '' could never do.

To colour a whole row, set Background on the repeater's row frame rather than on a cell inside it.

The aggregates: sum · count · avg · min · max · pluck · round · abs · upper · lower · concat · ifEmpty · format. Each takes a list and, where it needs one, the column to read: sum(claims, 'par_amount').

The list functions: where · exclude · take · skip · sortBy · groupBy · lookup · distinct. Every one of them names a column, never a condition — where(claims, 'status', 'Approved'), not where(claims, row.status == 'Approved'). That is a real limit rather than a style: a function is handed its arguments already worked out, so a condition would be answered once instead of once per row.

pluck is the one that answers with a list rather than a single value, and it is what a chart is bound to. A chart wants a series, and a dataset is rows — so pluck(revenue, 'amount') gives it the amounts and pluck(revenue, 'month') gives it the labels beside them, both out of the same dataset so they cannot fall a row out of step.

A chart​

Insert → Bar chart or Line chart puts a chart on the page under the middle of your view: a frame the report engine draws into, with a title. There is nothing in it to draw by hand — the scale, the ticks and the labels are worked out from the data each time the report runs. On a screen the same menu offers more kinds, as drawings; a report offers the two it can print.

Select the frame, press Write code, and fill in Chart values and Chart labels, both with pluck so they come out of one dataset in step: pluck(bystatus, 'total') and pluck(bystatus, 'par_status'). Bar or line, and the colour, are on the Report tab. The preview and the printed page draw the same chart.

A chart that does not fit under what is above it moves to the next sheet, like any other block.

Barcodes and QR codes​

Insert → Barcode or Insert → QR code puts a real, scannable code into the frame under the middle of your view: drop it on a page header and it prints on every sheet. On the Report tab set its Value, its Quiet zone (the blank margin a scanner needs), and either Print the value under the bars (Code 128) or Error correction (QR). Code 128 holds printable ASCII; a QR code holds any text, including a link.

For a code that is different on every row, select it inside the repeater, press Write code and fill in Barcode value: row.par_reference. The canvas draws the fixed value, because a canvas has no rows.

Paper and orientation​

The page frame's size is the paper, in points (72 to the inch): 595 × 842 is A4. To change the paper of a page, type its W and H; the Frame menu's Paper sizes draw a new frame.

For landscape, first tell the bands to follow the paper: set each band's Horizontal constraint to Left & right, and anything that sits on its right-hand edge to Right. Then make the page 842 × 595.

Margin, on the page's Report tab, is the space kept clear at the top and bottom of every sheet. Across the sheet, everything prints where you drew it. Print / PDF prints each page on a sheet exactly the page's size, so the browser has no margin to put its own header and footer in.

A report in Persian or Arabic​

Type the words and the designer turns the text right to left. A text whose words are Persian, Arabic or Hebrew is set Right to left and aligned to the right as you finish typing, and the page prints it that way: a full stop or a number at the end of a line stays at its end. A bound text has no words to go by, so set Right to left yourself on the Graphic tab, next to the alignment buttons. Lay the columns out from the right, as a Persian reader reads them.

The digits and the calendar are the reader's. A text whose Format is number or date is written in the language and calendar of whoever opens the report: Persian digits for a reader working in Persian, and a Jalali date for one whose Appearance settings → Calendar is Persian. The designer's Preview follows your own settings, the app follows each reader's, and the same report prints 7,890.50 for one and ۷٬۸۹۰٫۵۰ for the other.

A number inside concat is written as it is stored, so give it a format of its own: concat('صفحهٔ ', format(pageNumber, 'integer'), ' از ', format(pageCount, 'integer')).

Previewing a report printed for one record​

When the first dataset has a record field, the report is printed from one record, and the preview shows it that way: it takes the first row of the first dataset as the record, and narrows every dataset with a record field to it. An invoice previews as the first invoice and its own lines, not every invoice's.

A report you uploaded a template for​

Reports created by pasting a template have no drawing behind them. Pressing Design on one opens an empty canvas rather than trying to rebuild the layout from the template.

That is deliberate. A template records bands, repeaters and bindings — not the frames, guides and styles you arranged to arrive at them — so anything the platform drew from it would be a guess, and saving that guess would replace your real template with the guess. If you want an uploaded report on the canvas, lay it out once and save; from then on it round-trips.

The reverse is safe: editing a designed report's datasets through the Reports drawer leaves its drawing untouched.

Saving, and where it goes​

There is exactly one Save, and it is in the title bar at the top of the screen — the same place as on every other screen in the product. It writes the report as a solution component: layered, owned by your solution, validated, and ready to export.

The designer's own File menu is not shown here. Standalone, that menu saves a JSON file to your disk; inside the platform that would be a second command that looks like it stored your work and did not. Export (PNG, SVG) stays available, because a picture of a report is useful either way.

Moving it between environments​

Nothing changes: the report is a solution component and travels the way it always did, with its template inside it. Your drawing travels with it too, so the person who imports it can press Design and continue your layout rather than starting again.

Troubleshooting​

  • "This design has no report page yet." — you have drawn on the canvas but not inside a report page. Use Insert → Report page and put your content in it; the page frame is what tells the compiler where the report starts.
  • Save is greyed out. — nothing has changed since you opened it. Draw something first.
  • The report prints but the page is empty. — the repeater is not pointed at a dataset. Open Repeat over: it lists every dataset this report really has. Pick one, or add the missing dataset under Edit and then pick it.
  • "Every page frame is a section of this report, but they must all be the same size…" — one of your Report page frames is a different size from the first. Resize it to match; everything prints on the first frame's paper until you do. See Several page designs in one report, above.
  • My layout came back different. — you are on a report whose template was uploaded rather than designed. See A report you uploaded a template for, above.
  • A cell is blank and the rest of the report is fine. — its binding could not run. Open it: the editor underlines the part it could not make sense of. A binding that fails leaves a blank on purpose rather than taking the page down with it.
  • rows.length says 'length' is a method — write length() to call it. It is a method in BS Lang, so it needs its brackets: rows.length(). count(rows) reads better in a report anyway. (It used to answer with the whole list instead of the count, and say nothing. It does not now.)
  • sum is underlined in red. — check the spelling against the list above. The editor knows the report's own aggregates and nothing else; a function the platform does not have is a function nothing will call.
  • A binding written before this release stopped working. — report expressions are BS Lang now, the same language as everywhere else in the platform. Almost everything carries over unchanged — field paths, arithmetic, comparisons, ?:, the aggregates — but an expression that relied on something the old evaluator happened to allow is worth re-opening, because the editor will now tell you.