Report Designer

Manual for the Report Designer: building graphical reports from live data, Historian data, alarms, events, the audit trail, and project SQLite files, with PDF and Excel output, multi page templates, banded and free canvas layouts, calculations, and Python hooks.

Updated 34 min read
On this page (13 sections)
  1. Overview
  2. Creating a Report
  3. Report Period
  4. Pages and Layouts
  5. Components
  6. Columns
  7. Data Bindings and Placeholders
  8. Tables and Calculations
  9. Audit List
  10. Inline Expressions
  11. Python Hooks
  12. Executing, Scheduling, and Output
  13. API and AI Access

The Report Designer creates graphical reports without writing rendering code. A report is built from components such as headers, KPI tiles, trend charts, tables, alarm lists, audit lists, and text blocks, placed on one or more pages. The report is rendered server side to a PDF file, and additionally to an Excel file when the report is Excel compatible.

Reports read their data from the runtime variables, the Historian, the alarm and event history, the audit trail, and SQLite database files stored in the project folder, for example a database where totals or calculations are stored.

The reports of the project are listed on the Reports page in the configuration menu, where a report is created, enabled, described, given its output folder, executed, and deleted. The Report Designer action on a report row opens the layout of that report in the Report Designer.

In the designer, the left side shows the component Toolbox and the available data sources. The center shows the report page as it will print. The right side shows the Report Settings and the properties of the selected component. The top bar holds the Design, Preview, Output, and Script views, AI, Execute, and Save. Creating and deleting reports is done on the Reports page; the designer edits the layout of the report it was opened with.

A report is a configuration item of its own. It has an Enable setting and is executed with the Execute Report action or with RunReport from a Python script; no separate script entry is created for it.

Creating a Report

Press Add on the Reports page to create a report. The dialog asks for the name; the Report Designer action on the new row then opens it with a default page containing a header and a page footer. A report name may contain letters, digits, spaces, underscore, dash, dot, and parentheses, for example Daily Production Report, and may not start or end with a space. The Title in the Report Settings is free text and is used as the printed heading.

Report SettingDescriptionExample
NameUnique report name. Letters, digits, spaces, underscore, dash, dot, and parentheses. Also names the run status variables of the report.Daily Production Report
EnableGates execution by the Execute Report action and by RunReport. A manual Execute always runs the report.Enabled
DescriptionFree text description shown in the report list.Daily report for line 1
TitlePrinted report heading. Placeholders are allowed.Daily Production Report
PagePage size and orientation.A4, portrait
AccentAccent color used for the header line, table headers, and default chart colors. Press the color square next to the field to open the color picker.#2596BE
Excel compatibleWhen enabled the report also produces an Excel file. Only banded pages are allowed in this mode.Enabled
Output toRead only. Shows the folder the rendered files are written to. The destination is set with Output Path on the Reports page.Project/Reports

Press Save to store the report. Saving validates the complete template and reports any problems in the Output view. Press Execute to render the report immediately; the produced files open in the Preview view. Press the arrow next to Execute to run the report with an explicit period or with other parameter values.

Report Period

Every report has one period that period bound components use unless they override it: Historian aggregates, trend charts, alarm lists, event lists, audit lists, and the {Report.PeriodStart} and {Report.PeriodEnd} placeholders.

The period source is set in the Report Settings. With source preset, the period is resolved relative to the moment the report runs, so a scheduled report always covers the correct window. Available presets are yesterday, today, last24h, last7days, last30days, thismonth, and lastmonth. With source variables, the start and end are read from two variables at render time, for example Local variables maintained by shift logic. The variable values must parse as YYYY-MM-DD HH:MM:SS. The Start var and End var fields each have a picker button that searches the variables of the project.

When a report is executed manually or through the API, an explicit period can be passed to regenerate a report for a past window without changing the template.

A report that shows only live values still has a period, because every report has one, but nothing in it reads the period. Use a preset that ends at the moment of the run, today or last24h, so the period shown in the header matches what the report contains, or replace the header subtitle with {Report.Generated}.

A single component can cover a different window than the rest of the report. Select the component and fill in Start or End under Period in the Properties panel. Both fields are empty by default, which means the component follows the report period. This is how one document holds a daily, a weekly, and a monthly block: give each block its own Start.

An override applies to that component only. It shifts the data the component reads, the Hist aggregates in its expressions, and the {Report.PeriodStart} and {Report.PeriodEnd} it prints. The report keeps its own period for the file name, the page footer, and every other component. The band label in the Design view shows the override, so a component that does not follow the report period is visible in the layout.

A value that cannot be resolved at render time, and a start that is not before the end, are reported in the Output view and the component falls back to the report period, so a report always renders.

FormDescriptionExample
EmptyThe component covers the report period.
OffsetShifts the report period boundary. Units are m for minutes, h for hours, d for days, and w for weeks. The sign is required.-7d, +12h, -90m, -2w
TimestampA fixed moment, written as YYYY-MM-DD HH:MM:SS, YYYY-MM-DD HH:MM, or YYYY-MM-DD.2026-07-01 06:00
PlaceholderA parameter or hook value, resolved at render time. The value must parse as a timestamp.{Param.From}
ExpressionOne whole inline expression returning a datetime. Report.Start and Report.End are the report period, and timedelta is in scope.{= Report.End - timedelta(days=3) }

Pages and Layouts

A report contains one or more pages. Use the page tabs below the canvas to switch pages, add a page with the + tab, and delete the current page. Each page has its own layout mode, selected with the Banded and Free canvas buttons.

On a banded page, components flow from top to bottom in order and long content continues on additional printed pages automatically. Use the arrow buttons in the Properties panel to reorder components. Banded pages map directly to spreadsheet rows and are the recommended layout for reports that must export to Excel.

On a free canvas page, components carry an X and Y position and a width, and are placed anywhere on the page. Drag a component to reposition it; positions snap to a 10 pixel grid and can also be entered in the Properties panel. Free canvas pages render to PDF only, so they are not allowed while Excel compatible is enabled.

The page footer component repeats at the bottom of every printed page and supports the {Page} and {Pages} placeholders for page numbering.

Components

Click a component in the Toolbox to add it to the current page, or drag it onto the page. On a banded page an insertion line shows where the component will be placed; on a free canvas page the component is placed at the drop position. Click a component on the page to edit its properties. Delete component removes the selected component.

Duplicate places a copy of the selection on the same page. Copy, or Ctrl+C, places the selection on the clipboard, and Ctrl+V pastes it onto the current page, also on another page or in another report opened in the same browser. Pasted components keep their settings and are selected after the paste, so they can be moved as a group. On a free canvas page a paste is offset so it does not land exactly on top of an existing component, and the relative positions within a copied group are kept.

The Period section in the Properties panel gives a component its own window instead of the report period, which is described under Report Period.

An image is added without typing a path. Drag an image file from the file explorer onto the page and it is stored in the project folder and placed as an Image component at the drop position, or drop it on an existing Image component to replace its file. Upload image in the Properties panel of an Image component does the same through a file dialog. Accepted files are .png, .jpg, and .jpeg up to 10 MB. The file name is cleaned up, and a file with a name that already exists in the project folder is stored with a number added, so an existing image is never replaced. Because the file lands in the project folder, it travels with a project transfer or backup.

12 rows
ComponentDescription
HeaderReport title with subtitle and an accent line. Typically the first component of a page.
TextFree text block with styles Heading 1, Heading 2, Body, and Callout. Supports placeholders.
KPI rowA row of value tiles. Each tile has a label, a data binding, units, a number format, and an optional comparison with the previous period.
Trend chartLine, bar, or area chart of one or more variables over the report period, read from the Historian. Samples can be averaged per 15 minutes, hour, or day, or drawn raw. Each series has a color square that opens the color picker.
TableData table from a project SQLite query, the Historian, the alarm history, the event history, or the audit trail, with column formatting, calculations, and summary rows. See Tables and Calculations below.
Alarm listAlarms that occurred in the report period, with a minimum severity filter, an asset filter, and a row limit. When the row limit leaves alarms out, a red line below the list states how many of the alarms of the period are listed. See Tables and Calculations for the Asset setting.
Event listEvents that occurred in the report period, with an asset filter and a row limit. When the row limit leaves events out, a red line below the list states how many of the events of the period are listed. See Tables and Calculations for the Asset setting.
Audit listAudit trail records of the report period, with an asset, category, and user filter and a row limit. Every value is printed exactly as recorded and long values wrap. See Audit List below.
ImageImage from a file under the web root, for example a company logo. Press Upload image in the Properties panel, or drag an image file onto the page, to store the file in the project folder and fill in the path. A path can also be typed, relative to the web root, for example Project/logo.png. Width (%) sets the printed width as a percentage of the space the component has, which is the printable page width on a banded page and the component box on a free canvas page, and the height follows the proportions of the picture. Leaving Width (%) empty prints a third of the page width on a banded page and fills the box on a free canvas page. Align places the picture on the left, in the center, or on the right of that same space.
ColumnsComponents placed side by side on a banded page, for example a text next to an image. A columns component holds up to four columns, each with its own list of components. See Columns below.
SpacerVertical spacing between components on banded pages.
Page footerLeft and right footer text repeated on every printed page, with {Page} and {Pages} numbering.

Columns

A banded page places one component under the other. The Columns component puts components next to each other within that flow, for example a description beside a logo, or a short table beside a KPI row. Add it from the Toolbox like any other component. A new Columns component starts with two equal columns.

Fill a column by clicking it on the page and then clicking a component in the Toolbox, or by dragging a component from the Toolbox straight into the column. The selected column is outlined, and a component dropped between two components in a column is inserted at that position. A column holds any number of components, one under the other.

Click a component inside a column to edit it. The Properties panel then shows that component with a Columns button at the top that returns to the Columns component itself, and the arrow, Duplicate, Copy, and Delete component buttons act within the column. Copy places the component on the clipboard, so Ctrl+V pastes it onto the page as a normal component.

The Columns section of the Properties panel lists every column with its width and its components. Width is a percentage of the band. A column left empty shares the remaining width with the other empty columns, so two empty columns are two equal halves, and setting one column to 30 leaves 70 for a single empty column beside it. Use + column to add a column, up to four, and the cross to remove one. Removing a column removes the components in it.

A Columns component prints as one block on a single page. It moves to the next page when it does not fit on the current one, and content that runs past the bottom of the page is cut, with a note printed at the cut. Keep the content of a column short enough to fit on a page, and place long tables and lists directly on the page instead, where they continue onto the next page automatically.

A Columns component can hold any component except another Columns component and the page footer, and is available on banded pages while Excel compatible is disabled. A spreadsheet has no page width to divide, the same reason a free canvas page is not available in that mode. On a free canvas page, place components side by side with their X and Y positions instead.

Data Bindings and Placeholders

KPI tiles use a binding that selects where the value comes from. With the compare option enabled, the tile also computes the same aggregate over the previous period of equal length and shows the difference in percent.

BindingDescriptionExample
var:DeviceName.VariableNameLive value of the variable at render time.var:PLC1.Pump1_Speed
hist:sum:DeviceName.VariableNameHistorian aggregate over the report period. Available aggregates are sum, avg, min, max, count, and delta (last value minus first value).hist:sum:PLC1.Line1_Total
expr:keyValue computed by the Python hook script and stored in the report context.expr:oee
{= expression }Value calculated by an inline expression. The Format and Units of the tile still apply, and the compare option re-evaluates the expression over the previous period.{= Hist("PLC1.Line1_Total","sum") / 1000 }

Text fields, titles, and footers support placeholders that are resolved when the report renders. Any variable can also be used as a placeholder with its full name, for example {PLC1.Pump1_Speed}. Text fields also accept inline expressions written as {= expression }, which calculate a value in the template itself. See Inline Expressions.

11 rows
PlaceholderDescription
{Report.Name}The report name.
{Report.Title}The report title.
{Report.PeriodStart}Start of the report period.
{Report.PeriodEnd}End of the report period.
{Report.Day}Date of the period start.
{Report.Generated}Date and time the report was rendered.
{Expr.key}Value computed by the Python hook script.
{DeviceName.VariableName}Live value of any variable.
{Param.name}Value of a report parameter for this run, or its default.
{Page} and {Pages}Page number and page count, in page footers.
{= expression }Value calculated by an inline expression, for example {= Num("PLC1.Level") * 2 }.

A report parameter is a named value with a default, added under Parameters in the Report Settings. The value is used as a {Param.name} placeholder in texts, titles, footers, and the File Name, in variable names (the Variables of a Historian table, the variable of a trend chart series, and a var: or hist: binding, for example hist:sum:{Param.PLC}.Volume), in the Asset of alarm lists, event lists, audit lists, and tables, as Param.name inside an inline expression, as a bound SQL parameter inside a Project SQLite query, and as ctx.params in a hook script. Writing {Param.Line} in a query binds the value as a real SQL parameter instead of pasting it into the statement.

The defaults apply to every run. A single run overrides them from the arrow next to Execute in the Report Designer, from the params argument of RunReport, and from the parameters argument of the ExecuteReport tool. The Execute Report action always runs with the defaults. Parameter values are text; a number is converted to text before the report renders.

A field that holds nothing but a variable name has a picker button beside it, which opens a search box over the variables of the project and writes the name into the field. This covers the Start var and End var of the Report Settings, the Series of a trend chart, and the Variables of a Historian table, where the picker stays open so several variables can be added one after another. The Asset field of alarm lists, event lists, audit lists, and tables has the same kind of picker over the asset paths of the project.

Fields that accept text, placeholders, or an expression carry the expand button instead. The editor dialog it opens has the same variable search, and inserts a picked variable in the form the field needs, so the variable of a KPI binding or of a text is chosen there.

In both places, typing searches every variable of the project, and the number of remaining matches is reported when the list is longer than the results shown.

Tables and Calculations

A Table component reads its rows from one of five sources. Project SQLite reads from a SQLite database file in the offline project folder, using a single SELECT query. The {Report.Day}, {Report.PeriodStart}, and {Report.PeriodEnd} placeholders can be used inside the query and are bound as query parameters. Historian lists the raw samples of one or more variables over the report period, with the columns TimeStamp, Variable, and Value. Alarms and Events read the alarm and event history for the report period. Audit trail reads the audit trail records of the report period with the Asset, Categories, and User name filters of the Audit list, and prints them the same way, so it is the source to use when audit records need Group by or a summary row.

An Alarm list, an Event list, an Audit list, and a Table with the source Alarms, Events, Audit trail, or Historian have an Asset setting that keeps only the records of one asset and of everything below it. Enter the asset path as shown in the Asset Manager, for example Site/Line A, which also keeps Site/Line A/Pumps and Site/Line A/Valves but not Site/Line AB. The picker button beside the field lists the asset paths of the project, taken from the Asset Manager tree and from the assets set on variables, alarms, and events, with a search box that filters the list while typing; All assets (no filter) empties the field. Upper and lower case must match, and an empty field keeps every asset.

Alarms, events, and audit records are matched on the asset stored with each record when it was written, so after an asset is renamed, older records keep the old path. Audit records written before the audit trail recorded an asset have none and are left out by an asset filter; the Audit Trail article lists the asset each audit record carries. Historian samples are stored without an asset: for the Historian source, the table prints the variables that belong to the asset when the report runs, and the rows get an additional Asset column that can be used with Group by. A variable that was moved to another asset therefore reports under its new asset, also for its older samples.

With an Asset set, Variables narrows the variables of the asset instead of naming them one by one. Each line is a variable name or a pattern in which * stands for any text, for example *.Flow for every flow variable of the asset, and upper and lower case are ignored. An empty Variables list prints every variable of the asset. The picker button beside Variables then lists the variables of that asset only.

The Asset setting accepts placeholders, so one report serves every unit, and so do variable names: each line of Variables, the variable of a trend chart series, and the variable name of a var: or hist: KPI binding, for example {Param.PLC}.Flow or hist:sum:{Param.PLC}.Volume. For a batch report, write {Param.Unit} in Asset and run the report at the end of the batch with the batch start and end as the period, for example RunReport("Batch Report", start, end, {"Unit": "Site/Line A"}). When Max rows cuts an alarm, event, or historian table off, a red line below it states how many records it holds, followed by the asset filter. A query holds placeholders only, so that they stay bound as query parameters. An inline expression in a query is refused when the report is saved; put the calculation in a column or in a text component instead.

Without configured columns, the table shows all columns returned by the query. Configured columns control the header text, the number format, calculations, and the summary row. Group by sorts and groups the rows by a column and prints a group heading per group.

Column SettingDescriptionExample
Column nameName of the column in the query result. When the column holds JSON, the name of the column followed by a dot and a key prints only the value of that key, for example NewValue.Value. Further dots address a nested key or a position in a list. The key is matched without regard to upper and lower case, and a row without that key prints an empty cell.Produced
HeaderPrinted column header.Produced (pcs)
FormatPython number format specification applied to numeric cells.,.1f
AggregateSummary row calculation for this column: sum, avg, min, max, count, py:functionName to call a function from the hook script with the column values, or an inline expression over Values (the numeric cells of the column) and Raw (every cell).{= max(Values) - min(Values) }
Computed fnMakes this a computed column. Use an inline expression that reads the source row as Row['Column'], or the name of a hook script function that receives each row and returns the cell value.{= Row['Produced'] / Row['Hours'] }

Standard summary aggregates are written to the Excel export as native spreadsheet formulas, for example =SUM(...), so the delivered sheet stays calculable. Values computed by inline expressions and by Python functions are exported as plain values.

Audit List

The Audit list component prints the audit trail records of the report period, oldest first, from the same records the Audit Trail page shows, for example a daily list of configuration changes or a weekly list of operator actions.

Every value is printed exactly as it was recorded, without number formatting. Long values, such as the Old Value and New Value of a configuration change, wrap onto as many lines as they need instead of being cut off, and a record that is taller than a page continues on the next page with the column headings repeated. The column widths follow the content. A landscape page gives the long columns the most room, so select landscape under Page in the Report Settings for a report that holds an audit list.

The Excel file holds the same values, wrapped in their cells. A value longer than the 32767 characters an Excel cell holds is cut in the Excel file with a note, and the PDF of the same run prints it in full.

SettingDescriptionExample
TitlePrinted above the list. Placeholders are allowed.Configuration changes {Report.Day}
AssetOnly the records of this asset and of everything below it, matched on the asset stored with each record. Empty includes every asset. A {Param.name} placeholder is resolved for each run. See Tables and Calculations for the Asset setting and its picker.Site/Line A
CategoriesAudit trail categories to include, separated by commas: Config, Security, Operator, Records, and System. Empty includes every category. A name that is not one of these categories is refused when the report is saved. A {Param.name} placeholder is resolved for each run.Config, Operator
User nameOnly the records of this user, matched without regard to upper and lower case. Empty includes every user. A {Param.name} placeholder is resolved for each run.Operator1
Max rowsMaximum number of records printed, from 1 to 10000.1000
ColumnsColumns to print, each with an optional header text, computed value, and summary aggregate. Without columns the list prints Time, User Name, Category, Entity Type, Entity Name, Asset, Action, Old Value, New Value, Reason, and Signature.

The column names available under Columns are Time, UserName, Origin, SourceIp, Category, EntityType, EntityId, EntityName, Asset, Action, OldValue, NewValue, Reason, Signature, SignatureId, NodeId, PrevHash, and RecordHash. Time is the time stamp of the record in the local time of the server. Asset is the asset the action concerns, as listed in the Audit Trail article. Signature shows the electronic signature that authorized the record, written as the full name of the signer with the user name in parentheses, followed by the meaning, and stays empty for a record without a signature.

Old Value and New Value hold JSON. A configuration change stores the complete row before and after the change, and a variable write stores {"Value":"5"} as the old value and {"Action":"Set Value","Value":"7"} as the new value. To print a single key instead of the complete value, name the column after the source column followed by a dot and the key. For a list of variable writes, add the columns OldValue.Value and NewValue.Value with the headers Old and New, and filter the list on the Operator category; the two columns then print 5 and 7. A record without the selected key prints an empty cell, so choose keys that fit the records the list holds.

Below the list, the report states how many of the matching records it holds, followed by the active filter, for example 120 of 120 records listed. Filter: Asset Site/Line A; Category Config. A list without records prints No records in the period. When Max rows cuts the list off, the line is printed in red, for example 1000 of 1342 records listed. Max rows (1000) cut the list off; raise Max rows to list every record. Raise Max rows, or shorten the period, until the list is complete.

Printing audit records is itself recorded in the audit trail. Every run of a report that holds an audit list, or a table with source Audit trail, adds one records management entry with category Records, action Export, and entity type AuditTrail. The entry holds the report name, the produced files, and for each list the period, the filter, and the number of records printed and found. It carries the name of the user who started the run, or System when a schedule, an alarm, or an event started it.

The report reads the audit trail without checking the rights of a user, because a scheduled run has no user. Everyone who can open the Output Path folder of the report, or a Document Viewer that shows its files, can read the printed audit trail, so set the Output Path and the Document viewer access with that in mind.

Inline Expressions

An inline expression calculates a value inside the template, without a Python hook script. Write it as {= expression } in any text field, title or footer, in a KPI tile binding, in the Computed fn of a table column, and in the Aggregate of a table column. Braces cannot be used inside the expression, so an expression is a single calculation and not a sequence of statements.

In a text field the expression is resolved before the {Placeholder} values are substituted, and it works on typed values. {= Hist("PLC1.Line1_Total","sum") / Report.Hours } therefore divides a number rather than formatted text. In a KPI tile binding, in Computed fn and in Aggregate the whole setting is the expression, so that the calculated value keeps its type and the Format setting still applies.

The following names are available inside an expression.

14 rows
NameDescription
Report.Name, Report.TitleReport name and report title.
Report.Day, Report.PeriodStart, Report.PeriodEnd, Report.GeneratedThe same text values as the matching placeholders.
Report.Start, Report.EndStart and end of the report period as date and time values.
Report.Hours, Report.DaysLength of the report period as a number.
Param.nameValue of a report parameter for this run.
Expr.keyValue stored in the report context by the hook script.
Var("Name")Live value of a variable as text.
Num("Name", default)Live value of a variable as a number, with a default when it is missing or not numeric.
Hist("Name", "sum", default)Historian aggregate over the report period. Available aggregates are sum, avg, min, max, count and delta.
Val(x, default)Converts a cell or a text to a number, with a default when it cannot be converted.
math, datetime, timedeltaThe Python math module and the date and time types.
abs, min, max, round, sum, len, sorted, float, int, str, formatThe usual value functions.
Row['Column'], Row.ColumnThe source row of a computed table column.
Values, RawThe numeric cells and all cells of a column, in a summary aggregate.

Row is available in a computed column only, and Values and Raw in a summary aggregate only, so that an expression placed in the wrong setting reports the unknown name instead of reading unrelated data.

An expression that cannot be evaluated never stops a report. The text keeps the unresolved {= expression }, a KPI tile renders empty, a table cell shows ERR: with the reason, and the reason is written to the run output shown by the Run dialog. Expressions are checked when the template is saved: a syntax error, an expression longer than 2000 characters and the use of '__' are reported as validation errors.

The same list of names is shown in the Report Designer under Report Settings, in the Expressions section, so it is available while editing a report.

Examples:

SettingExpressionResult
TextTotal: {= Hist("PLC1.Line1_Total","sum") / 1000 } m3Period total converted from litres to cubic metres.
Text{= round(Num("PLC1.Pump1_Speed") / 1500 * 100, 1) } %Live speed as a percentage of the rated speed.
KPI tile binding{= Hist("PLC1.Line1_Total","sum") / Report.Hours }Average per hour over the report period.
Computed fn{= Val(Row['Produced']) - Val(Row['Rejected']) }Accepted quantity per row.
Aggregate{= max(Values) - min(Values) }Range of the column in the summary row.
FooterLine {= Param.Line } - page {Page} / {Pages}A parameter value combined with the page placeholders.
KPI tile binding{= Expr.target - Hist("PLC1.Line1_Total","sum") }Remaining quantity, combining a value computed by the hook script with a Historian aggregate. Put the shared or complicated part in the hook and keep the arithmetic here.

Fields that accept placeholders and expressions carry an expand button, which opens the field in an editor dialog. The dialog is the practical way to write anything longer than a short placeholder, because the properties panel is narrow. Placeholders and expressions are coloured while typing, and the Insert list on the right adds a name at the cursor position. It lists the available names, and typing in its search box looks up the variables of the project and lists the first matches, reporting how many more match so that the search can be refined.

The dialog works in two modes, matching the field. A text field keeps prose, placeholders and expressions mixed. A KPI tile binding, a Computed fn and an Aggregate hold one whole expression, and the dialog reports it immediately when an expression is mixed with other text there. These three fields also accept their plain forms, such as var:VariableName, a hook function name or sum, which the dialog leaves alone.

Every expression in the field is checked for syntax while typing, so an unclosed bracket is reported in the dialog instead of when the report is saved. Press Evaluate to resolve the field against the report's current period and see the value it produces. For a single expression the value is shown with its type, which is what determines whether the Format setting of a KPI tile applies. Evaluate runs the hook script's prepare_report(ctx) when the template names one, exactly as rendering does, and writes no files. Row, Values and Raw only exist while a table is being built, so an expression that uses them is checked by running the report.

Python Hooks

A hook script is meant for logic that an inline expression cannot express, such as a calculation over several queries or a value that is reused by many components. Reports integrate with Python Scripts through a hook. Select a Module script in the Report Settings; when the report renders, the module is imported and its prepare_report(ctx) function is called before any component is drawn. Values stored in the context become available as expr: bindings and {Expr.key} placeholders. The same module provides the functions used by py: aggregates and computed table columns. The name of a Module script must be a valid Python identifier, using letters, digits and underscores and not starting with a digit, because the renderer imports it by name.

Example
def prepare_report(ctx):
    total = GetFloatValue("PLC1.Line1_Total")
    ctx["oee"] = compute_oee(ctx.period_start, ctx.period_end)

def weighted_oee(row, ctx):
    return row["Produced"] / max(1, row["Produced"] + row["Rejected"]) * 100

def shift_average(values, ctx):
    numeric = [float(v) for v in values if v is not None]
    return sum(numeric) / len(numeric) if numeric else 0

The context provides ctx.period_start and ctx.period_end as the resolved report period, and behaves as a dictionary for storing computed values. All standard script functions such as GetFloatValue and SetValue are available inside the hook. A computed table column calls a function that receives (row, ctx) and returns the cell value, and a py: summary aggregate calls a function that receives (values, ctx) and returns the summary value. An inline expression can read anything the hook stored in the context, so the shared or complex part can live in the hook while the per component calculation stays in the template.

Executing, Scheduling, and Output

Execute renders the report immediately, also when the report is disabled. The produced files are shown in the Preview view and are named after the report and the period start date, for example Daily Production Report_2026-07-17.pdf. The Output view shows the render log and any errors, and the Files action on the Reports page lists the rendered files of a report for download.

A report is run unattended with the Execute Report action: select the report with the picker on the Report field in a Schedule action, an alarm action, or an event action. The same action is available on a page object, so an operator can render a report from a Button, and it runs with the period and the parameter defaults of the template. Who may press that button is set with User Groups on the report. Combine it with a Document Viewer bound to System.ReportFile_ReportName to show the result on the same screen. Saving an action that names a report that does not exist is rejected. From any Python script, RunReport renders a report as a step of the script, optionally with an explicit period and parameter values, and combines with SendEmail to mail the produced files. Both paths require the report to be enabled.

RunReport takes the report name, an optional start and end of the period, and an optional dictionary of parameter values: RunReport(name, period_start, period_end, params). Leaving the period out uses the period of the template, so a report with period source variables reads its two variables at that moment. Passing a start and end overrides the template period for that run, which is how a report is regenerated for a past window. The timestamps are written as YYYY-MM-DD HH:MM:SS.

Every run publishes its state on the system variables System.ReportStatus_ReportName and System.ReportMsg_ReportName, where ReportName is the report name with every character that is not a letter, digit, or underscore replaced by an underscore, so a report name with spaces stays usable in alarm and event expressions. The status value is Running, Idling, Error, or TimedOut. Both variables exist from startup; an empty value means the report has not run since the server started. Deleting a report removes its variables, and renaming a report moves them to the new name. Overlapping runs of the same report are skipped while it is still rendering.

Manual report execution from the Reports page, the designer, or an AI or MCP client is recorded in the audit trail and is available on the primary node only. A run started with the Execute Report action from a page object is recorded in the audit trail under the name of the operator. A run started by a schedule, an alarm, or an event is recorded in the Event log as Execute Report followed by the report name. A report that prints the audit trail also adds an export entry to the audit trail on every run, whoever started it; see Audit List.

A rendered report can also be shown directly on a runtime page with the Document Viewer object, which serves a file over an authenticated address once an administrator has listed its folder in the Document viewer access parameter, so the output folder does not have to be published for anonymous access.

Every report also publishes the full path of its most recent rendered file on System.ReportFile_ReportName, using the same name conversion as the status variables. Point the Document Path of a Document Viewer at that variable and the page always shows the latest run of that report, without the page having to know how the file is named. The variable is filled at startup from the output folder, so a restart does not blank the page, and it is updated after every successful run; a failed run leaves the previous document on screen. To let an operator choose an OLDER report, set the Document Folder of a DropDown or ListBox to the output folder and bind its variable to the Document Path.

Example
# The period of the template, so a variable period reads its two variables
RunReport("Daily Production Report")

# An explicit period: yesterday morning to this morning
RunReport("Daily Production Report", "2026-07-29 06:00:00", "2026-07-30 06:00:00")

# A period taken from variables an operator filled in on a page
RunReport("Daily Production Report",
          GetStringValue("ReportStart"),
          GetStringValue("ReportEnd"))

# Parameter values only, keeping the period of the template
RunReport("Daily Production Report", params={"Line": "Line 2", "Shift": "A"})

# Both, and mail the result
RunReport("Daily Production Report", "2026-07-29 06:00:00", "2026-07-30 06:00:00",
          {"Line": "Line 2"})
SendEmail("production@plant.local", "Daily production report",
          attachments=[GetStringValue("System.ReportFile_Daily_Production_Report")])

The PDF file is always produced. When Excel compatible is enabled, an Excel workbook is produced next to it with one worksheet per report page, native charts, and native summary formulas. Reports are part of the project: templates export and import with the project and replicate to the standby server in redundant setups.

The rendered files are written to the folder set in Output Path on the Reports page. Open the Settings action of a report, or edit the Output Path column in the report list, and enter the destination.

An empty Output Path writes into the Reports subfolder of the project folder. Any other value must be a full path, such as D:\PlantReports for a local drive or \\Server\Share\Reports for a plant file share, and is used exactly as entered. The path is checked when the report is saved: a relative path such as Reports/Daily is refused, and a path containing .. is refused.

Output Path belongs to the report and not to its template, so the Report Designer shows it read only in Report Settings. The Files action lists the files in the configured folder that carry the report name as prefix, so a shared destination keeps the rest of its content visible only to whoever owns it. Rendered files stay where they are written: housekeeping of the destination folder is the responsibility of the site, which is what allows a plant file share to keep its own archiving policy.

Report SettingDescriptionExample
EnableGates execution by the Execute Report action and by RunReport. A manual Execute from the Reports page, the designer, or an MCP client always runs the report.Enabled
NameUnique report name. Also names the run status variables of the report.Daily Production Report
DescriptionFree text description shown in the report list.Daily report for line 1
User GroupsGroups whose members may run this report from a page with the Execute Report action. Empty means Administrators only. Administrators always pass. Does not affect the Reports page, the designer, the API, MCP, RunReport, or an unattended alarm, event or schedule action.Operators, Maintenance
Output PathFolder the rendered PDF and Excel files are written to. Empty writes into the Reports subfolder of the project folder. Any other value must be a full path, including a UNC path to a file share, and is used as given.D:\PlantReports
File NameNames the rendered files, without extension. Empty uses the report name followed by the start date of the period. A pattern starts with {Report.Name}_ and accepts the placeholders of a text component plus a date and time pattern on the period.{Report.Name}_{Report.Start:%Y%m%d_%H%M}

The names of the rendered files come from File Name on the Reports page. Leaving it empty uses the report name followed by the start date of the period, for example Daily Production Report_2026-07-30.pdf. A pattern accepts the same placeholders as a text component, so {Report.Name}, {Report.Day}, {Param.name}, {Expr.key} and an inline {= expression } all work, and the period can be formatted with a date and time pattern such as {Report.Start:%Y%m%d_%H%M}.

A pattern starts with {Report.Name}_ and is checked when the report is saved. The Files action identifies the files of a report by that leading report name, so keeping it in place is what allows the list to work on a folder that also holds other content. The extension is added for each produced file, so the pattern itself carries no extension, and it names a file rather than a path: the folder is set with Output Path.

A pattern that includes a time resolves to a new file on every run, so every run is kept as a separate file. A pattern that resolves to the same name, such as the built-in one for a daily report, replaces the file of an earlier run on the same day.

User Groups on a report works the same way as User Groups on a Python script. It lists the groups whose members may run the report from a page with the Execute Report action, for example Operators, Maintenance. Leaving it empty means only Administrators can run it from a page, which is the value a new report starts with. Members of Administrators always pass. An operator who is not in one of the listed groups gets the message User does not have the rights to execute this report, and nothing is rendered.

The field applies to the page action only. The Reports page, the Report Designer, the configuration API and MCP are administrator paths and are unaffected, and an alarm, event or schedule action runs unattended, so there is no user to check. RunReport in a Python script is also unaffected: the rights of a script are its own User Groups.

API and AI Access

Reports are available through the configuration API under /API/Config/Reports with the same list, insert, update, and delete operations as other configuration entities. The report template itself is a JSON document stored with the report, so the AI assistant and MCP clients can create and adjust reports by editing the template through the configuration entity tools, and can render a report with the ExecuteReport tool. A report changed this way opens in the Report Designer with the changes applied, and a report changed in the designer is read back the same way, so both editing paths stay interchangeable.

The destination folder is a field of the report itself, outputPath, next to name, enable, and description. An update that omits it keeps the stored destination, and an update that sends it empty resets the report to the Reports subfolder of the project folder.

Something missing or wrong in this article? Contact us