---
title: "Register Reports"
description: "Register Reports to surface metrics and analysis on your objects."
---

import { Tabs, TabItem } from '@astrojs/starlight/components';

<div style="padding: 14px 16px; border: 0.5px solid #AFA9EC; border-radius: 12px; background: #EEEDFE; margin-bottom: 1.5rem;">
  <div style="display: flex; align-items: center; gap: 7px; margin-bottom: 8px;">
    <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
      <path d="M9 2L4 9h4l-1 5 5-7H8l1-5z" stroke="#7f77dd" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
    </svg>
    <span style="font-size: 11px; font-weight: 500; letter-spacing: 0.05em; text-transform: uppercase; color: #534AB7;">TL;DR</span>
  </div>
  <div style="font-size: 13px; color: #3C3489; line-height: 1.65;">
    A <strong>Report</strong> is reusable analytics that runs on a job's output. In <strong>Resources → Reports</strong>, pick the object type it runs on (Data Element, Feature, Model, Policy), write a <strong>Report Definition</strong> (Spark, Pandas, or Python) that returns a summary Pandas DataFrame, then add <strong>Figures</strong> that read that DataFrame (as <code>raw_output</code>) and render it as a table, Plotly chart, Markdown, Excel, or PDF. Optionally add <strong>Metrics</strong> to track a single number across recurring runs. Select the report when you run a job and it renders as its own tab.
  </div>
</div>

## What is it?

A **Report** is a piece of reusable analytics that runs on a job's output. For example, a decile report that runs on every model simulation, or a fair-lending report that runs on every policy simulation. You define the calculation once and the platform runs it on every relevant job.

A report has two layers: a **Report Definition** that computes the statistics and returns a Pandas DataFrame (its return value is called `raw_output`), and one or more **Figures** that read `raw_output` and render it as a table, chart, Excel download, Markdown note, or tracked Metric. The report renders as its own tab in the job results, with all its figures inside.

Registering reports gives you:

* **A shared visualization library** - build up a catalog of reports with consistent design across every object, instead of re-creating the same charts for each job.
* **Reuse across the platform** - once configured, a report is available wherever its object type runs (Data Element, Feature, Model, Policy), and from the Notebook.
* **Built-in governance** - reports move through the same access controls and approval workflow as other registered objects.

## Before you start

* Know which object type the report targets (Data Element, Feature, Model, Policy). Each type exposes different columns in the job output, so a report for one type usually won't work for another.
* If you plan to reuse a calculation across reports, register it first in **Resources → Global Functions**. See [Reuse a calculation with a Global Function](#reuse-a-calculation-with-a-global-function).

## How to Create a Report

Follow these steps to create a Report.

<details open class="step">
<summary>

#### Open the Create Form

</summary>

Go to **Resources → Reports** and click **+ New Report**. Give the report a clear name (for example, `Decile Table`, `ROC Curve and AUC`).

</details>

<details open class="step">
<summary>

#### Choose what objects the report runs on

</summary>

Set the **Object Type** <span style="color: #E5484D;">\*</span> - the type of object this report runs on, such as Model, Policy, Data Element, or Feature.

Some object types then show an **Object Subtype** to narrow the report further, and some don't. It is a multi-select: for a Data Element or Feature you pick one or more feature types; for a Model you pick one or more algorithm types (Binary Classification, Regression, and so on). For types like Policy or Quality Profile, no subtype field appears.

</details>

<details open class="step">
<summary>

#### Write the Report Definition (Formula)

</summary>

There are two optional fields: **Resources**, to pull in registered Global Functions the definition can call, and **Parameters**, to accept values the user supplies at run time. Expand for more detail.

<div class="collapse-section" data-summary="Resources (optional)">

A multi-select of registered Global Functions. Each one you select becomes callable in the definition by its alias. Use this when the definition reuses a calculation registered in **Resources → Global Functions**.

</div>

<div class="collapse-section" data-summary="Parameters (optional)">

Values the user supplies when running the job, so the report can be reused at different settings instead of hard-coded. Click **+ Add Parameter**; each has a **Name**, **Alias**, **Is Mandatory**, a description, and a **Type**. Common use: a configurable classification threshold, or a decile count.

The **Type** decides how the value reaches your definition:

* **String** - a single text value, resolved by alias (or `None` if optional and unset).
* **Number** - a single numeric value, resolved by alias (or `None` if optional and unset).
* **Single Object** - a registered object (for example a Data Element or Feature) whose values arrive as a column on `data`, accessed as `data["<alias>"]`.

</div>

Write the **Report Definition** <span style="color: #E5484D;">\*</span> in the code editor: the logic that computes the statistics. It must **return a Pandas DataFrame**, whether you write it in Spark, Pandas, or Python.

**Variables you can use in the definition:**

* **`data`** (PySpark DataFrame): the job's result data, including every input and output column. It is the same sample results shown in the **Job Result** section; run a job and open Job Result to see them. Expand below to see how job results look like.

<div class="collapse-section" data-summary="What data looks like">

For the full reference, see [Understand Job Output](/user-guide/how-tos/understand-job-output/).

Each row is one entity, `id` is the platform key, inputs sit under their own alias, and the result lands in `output`. The columns follow a consistent naming pattern per object type. Each tab lists the full set of columns, then shows a trimmed sample:

<Tabs>
  <TabItem label="Data Element">

Columns:

* `id` - the platform key.
* the source column - the raw input the Data Element reads.
* `output` - the Data Element's value.

The table below shows those columns for a Data Element that reads `annual_inc`:

<table class="attr-table lowercase-headers">
  <thead>
    <tr><th><code>id</code></th><th><code>annual_inc</code></th><th><code>output</code></th></tr>
  </thead>
  <tbody>
    <tr><td>A1001</td><td>82000</td><td>82000</td></tr>
    <tr><td>A1002</td><td>54000</td><td>54000</td></tr>
  </tbody>
</table>

  </TabItem>
  <TabItem label="Feature">

Columns:

* `id` - the platform key.
* one column per input - each named by its alias.
* `output` - the Feature's value.

The table below shows those columns for a Feature with a single input `fico`:

<table class="attr-table lowercase-headers">
  <thead>
    <tr><th><code>id</code></th><th><code>fico</code></th><th><code>output</code></th></tr>
  </thead>
  <tbody>
    <tr><td>A1001</td><td>742</td><td>A</td></tr>
    <tr><td>A1002</td><td>605</td><td>C</td></tr>
  </tbody>
</table>

  </TabItem>
  <TabItem label="Model">

Columns:

* `id` - the platform key.
* one column per input - each named by its alias.
* one column per transform - if the Model defines any.
* `dependent` - the target.
* `output` - the model score.
* `explainer` - a single column, when a Model Explainer is enabled (a map of contributions or a list of reason codes).

The table below is trimmed to a couple of inputs; a real Model has one column per input:

<table class="attr-table lowercase-headers">
  <thead>
    <tr><th><code>id</code></th><th><code>fico</code></th><th><code>dti</code></th><th><code>dependent</code></th><th><code>output</code></th></tr>
  </thead>
  <tbody>
    <tr><td>A1001</td><td>742</td><td>0.31</td><td>0</td><td>0.08</td></tr>
    <tr><td>A1002</td><td>605</td><td>0.52</td><td>1</td><td>0.63</td></tr>
  </tbody>
</table>

  </TabItem>
  <TabItem label="Policy">

Columns:

* `id` - the platform key.
* one column per input - each named by its alias.
* `<alias>` - for each offer-config and framework input, and for each derived variable.
* `block-<n>` - for each strategy block, where `n` is the block's position in the workflow.
* `segment-<s>.<n>` - for each segment, where `s` is its strategy block and `n` is the segment's position.
* `rule-<s>.<n>.<r>` - for each rule, where `s` is the strategy block, `n` is the segment, and `r` is the rule's position.
* `output` - the policy decision.

Segment and rule columns are booleans (`True` / `False` / `None`); `block-<n>` and `output` are outcome strings (`Pass` / `Fail`, or a custom outcome). The table below is trimmed to one input, one block, and one rule; a real Policy has a column for every input, block, segment, and rule:

<table class="attr-table lowercase-headers">
  <thead>
    <tr><th><code>id</code></th><th><code>fico</code></th><th><code>block-1</code></th><th><code>segment-1.1</code></th><th><code>rule-1.1.1</code></th><th><code>output</code></th></tr>
  </thead>
  <tbody>
    <tr><td>A1001</td><td>742</td><td>Pass</td><td>True</td><td>True</td><td>Pass</td></tr>
    <tr><td>A1002</td><td>605</td><td>Fail</td><td>False</td><td>None</td><td>Fail</td></tr>
  </tbody>
</table>

  </TabItem>
</Tabs>

Run a job for the object once and open its **Job Result** to see the exact columns for your case.

</div>

* **`entity`**: a Corridor Python object you can query for information about the object the job ran on. For a Model simulation, `entity.algorithm_type` returns `'Binary Classification'`, `'Regression'`, and so on.
* **`job`**: a Corridor Python object you can query for information about the job itself. For example, `job.job_type` returns `'Simulation'`, `'Monitoring'`, and so on.

* **Selected Global Functions**: available by alias.

  <div class="collapse-section" data-summary="More on Global Functions">

  Every Global Function you selected under **Resources** is callable directly, for example a function with alias `get_mean` is called as `get_mean(...)`. A Global Function is only in scope where you selected it; to use the same one in a figure or metric, select it again under that output's **Resources**.

  </div>

* **Report Parameters**: available by alias.

  <div class="collapse-section" data-summary="More on Report Parameters">

  `String` and `Number` parameters resolve to a single value (or `None` if optional and unset). `Single Object` parameters arrive as a column on `data`, accessed as `data["<alias>"]`.

  </div>

**Example: an ROC curve summary for a model**

This definition reads the model score and the binary dependent, computes the ROC points (false-positive rate, true-positive rate) and the AUC, and returns one Pandas DataFrame with columns `fpr`, `tpr`, and `auc`.

Whatever DataFrame you return here is what Figures and Metrics read, under the name `raw_output`. They also use its exact column names (`fpr`, `tpr`, `auc`), so if you rename a column in definition, any figure still reading the old name breaks.

```python
import pandas as pd
from sklearn.metrics import roc_curve, roc_auc_score

# 'output' is the model score; 'dependent' is the binary target
pdf = data.select("output", "dependent").toPandas()
scores = pdf["output"].tolist()
labels = pdf["dependent"].tolist()

fpr, tpr, _ = roc_curve(labels, scores)
auc = roc_auc_score(labels, scores)

# one row per ROC point; auc is the same scalar on every row
return pd.DataFrame({"fpr": fpr, "tpr": tpr, "auc": auc})
```

If a piece of the definition is something you compute in more than one report, register it once as a **Global Function** and call it by alias instead of copying it. Good candidates are small, reusable statistics, for example an AUC calculation, quantile or summary stats on a score, or binning a variable into deciles. See [Reuse a calculation with a Global Function](#reuse-a-calculation-with-a-global-function).

</details>

<details open class="step">
<summary>

#### Add Figures

</summary>

The Figures section opens with a **Figure 1** block; use **Add Figure** for more, and drag the grip handle to reorder. For each figure, set its **Width** <span style="color: #E5484D;">\*</span>, optionally select **Resources** (Global Functions to call), and write the **Definition** that produces the visualization.

A figure's Definition reads the same `raw_output` the Report Definition returned and renders it. Depending on what you return, it shows differently:

* **Pandas DataFrame** - renders as a table.
* **Plotly figure** - renders interactively.
* **Markdown string** - renders as formatted text.
* **Pandas ExcelWriter** - offered as a downloadable Excel file (not shown inline).
* **ReportLab Canvas** - a PDF.

Inside a figure's Definition you can access `raw_output`, `entity`, and `job` (the same objects as in the Report Definition), plus any **Global Functions** you select for this figure and any String or Number **Parameters** by alias. 

Differences from the Report Definition: a figure does not receive `data` (so `Single Object` parameters are not accessible), and Global Functions are per-output (select them again under the figure's **Resources**).

Continuing the ROC example, `raw_output` has columns `fpr`, `tpr`, and `auc`. Here is that same result rendered five ways.

<div class="collapse-section" data-summary="Figure Output: Table (Return a DataFrame)">

Return `raw_output` (or any DataFrame) to render it as a table.

```python
return raw_output
```

</div>

<div class="collapse-section" data-summary="Figure Output: Plotly Figure">

Return a Plotly figure to render it interactively. This draws the ROC curve.

```python
import plotly.graph_objects as go

auc = raw_output["auc"].iloc[0]

fig = go.Figure()
fig.add_trace(go.Scatter(x=raw_output["fpr"], y=raw_output["tpr"],
                         mode="lines", name=f"ROC (AUC = {auc:.3f})"))
fig.add_trace(go.Scatter(x=[0, 1], y=[0, 1], mode="lines",
                         line=dict(dash="dash"), name="Random"))
fig.update_layout(xaxis_title="False Positive Rate", yaxis_title="True Positive Rate")
return fig
```

</div>

<div class="collapse-section" data-summary="Figure Output: Markdown">

Return a formatted Markdown string, for example a one-line AUC summary.

```python
auc = raw_output["auc"].iloc[0]

return f"**AUC:** {auc:.3f}"
```

</div>

<div class="collapse-section" data-summary="Figure Output: Downloadable Excel">

Return a Pandas ExcelWriter to offer the result as an Excel download.

```python
import io
import pandas as pd

writer = pd.ExcelWriter(io.BytesIO())
raw_output.to_excel(writer, sheet_name="ROC")

return writer
```

The Excel content is not shown inline; the dashboard offers it as a download.

</div>

<div class="collapse-section" data-summary="Figure Output: PDF">

Return a ReportLab Canvas to produce a PDF.

```python
import io
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4

auc = raw_output["auc"].iloc[0]

c = canvas.Canvas(io.BytesIO(), pagesize=A4)
c.drawString(100, 100, f"AUC: {auc:.3f}")
return c
```

</div>

Each figure also has an optional **Enable Custom Comparison** toggle, for building a single combined visualization across two jobs instead of showing them side by side. Expand below if you need it.

<div class="collapse-section" data-summary="Enable Custom Comparison (optional)">

When you compare two jobs, each figure is shown side by side by default (comparing sim 1 and sim 2, two ROC curves appear next to each other). Turn on **Enable Custom Comparison** on a figure only when you want a single combined visualization instead, for example both ROC curves on one set of axes.

The figure's Definition then works against both jobs and exposes:

* `base_raw_output`, `compared_raw_output` - the Report Definition output from each job.
* `base_entity`, `compared_entity` - the Corridor object for each job.
* `base_job`, `compared_job` - the job object for each side.
* Report **Parameters** by alias (from the base job).

Custom comparison is only available for figures, not metrics, and a custom-comparison figure spans the full dashboard width. Additional figures inherit the same variables (`base_raw_output`, `compared_raw_output`, and so on).

</div>

</details>

<details class="step">
<summary>

#### Add Metrics (optional)

</summary>

A **Metric** is a single numeric value tracked across recurring job iterations: each iteration recomputes it, and the platform generates a trend graph automatically. You set the upper and lower thresholds for alerting later, at job-run time: when you select this report on the job form, a **Set Threshold** control appears for each metric where you enter the lower and upper bounds.

Click **Add Metric** to add one. Each metric has its own **Resources** and **Definition**. The Definition returns a single number and can access `raw_output` and `job`.

Continuing the ROC example, this metric returns the AUC from the same `raw_output`:

```python
return float(raw_output["auc"].iloc[0])
```

Return the metric value directly, with no wrapper. Each metric output tracks exactly one number, so to track two (say AUC and KS), add two Metric outputs. No visualization is needed: on each recurring run the platform recomputes the metric and builds a trend report, found under the **Tracking** tab, so you can watch it drift over time and alert on it.

</details>

<details open class="step">
<summary>

#### Fill in Properties and Create

</summary>

Fill in the [Properties](/user-guide/how-tos/common-registration-info/#properties) (Description, Group, and Keywords), then click **Create** at the bottom right. The Report is saved as a draft and you land on its details page.

</details>

## Running the Report

To run a report, select it in the **Reports** section of the job form when you [Run a Simulation](/user-guide/how-tos/run-a-simulation/). Each selected report runs on the job's output and renders as its own tab in the results.

To run a report on every job by default, add it to your **Default Reports** in settings. Default reports are preselected on new jobs for the matching object type.

## Reuse a calculation with a Global Function

If a calculation shows up in more than one report, register it once as a **Global Function** in **Resources → Global Functions**, then select it under **Resources** in any report that needs it and call it by its alias. This keeps a single definition of that calculation instead of copies drifting apart across reports.

<div class="collapse-section" data-summary="Example Global Functions">

Small, self-contained statistics are the natural fit. An AUC helper:

```python
from sklearn.metrics import roc_auc_score

def roc_auc(scores, labels):
    return float(roc_auc_score(labels, scores))
```

Quantile stats on a score:

```python
import numpy as np

def score_quantiles(scores):
    q = np.percentile(scores, [10, 25, 50, 75, 90])
    return {"p10": q[0], "p25": q[1], "p50": q[2], "p75": q[3], "p90": q[4]}
```

Binning a variable into deciles:

```python
import pandas as pd

def decile_bins(values):
    return pd.qcut(values, 10, labels=False, duplicates="drop")
```

Once registered and selected, call any of them in a Report Definition (or a figure) by its alias, for example `auc = roc_auc(scores, labels)`.

</div>

## What's next

* [Run a Simulation](/user-guide/how-tos/run-a-simulation/) with this report selected to see it in action.
* [Set up Alerts](/user-guide/how-tos/setup-alerts/) on a tracked Metric to get notified when it crosses a threshold.
