JSON Report Definition Language v1.0 — Custom Reports Schema Reference
JRDL (JSON Report Definition Language) is a declarative JSON format for defining custom data reports in Papereg. A single JRDL document fully describes:
JRDL is the portable format used for exporting and importing custom reports between workspaces.
Custom Reports let you build your own dashboards by pulling data from one or more forms in your workspace, applying aggregations (count, sum, average, etc.), and displaying results as summary cards, charts (bar, line, doughnut, pie, radar), and sortable tables.
Papereg ships with Preset Reports tied to industry templates (HR, Healthcare, Construction, etc.). Custom Reports extend this by letting you build any report from any form data — using the visual builder or raw JRDL JSON.
Version:
The current JRDL schema version is 1.0.
All definitions must include "version": "1.0".
The simplest JRDL document counts submissions from a single form:
{
"version": "1.0",
"sources": [
{
"alias": "intake",
"form_name": "Patient Intake",
"field_mappings": {
"patient_name": "full_name"
}
}
],
"layout": {
"summary_cards": [
{
"id": "card_total",
"title": "Total Patients",
"source": "intake",
"aggregation": "count",
"color": "primary"
}
],
"charts": [],
"tables": []
},
"filters": {
"default_date_range": "30d"
}
}
There are two ways to create custom reports:
A 5-step wizard in the UI: Info → Sources → Layout → Filters → Review. No JSON required. Best for most users.
Write or edit JRDL directly via the API or the Export → Edit → Import workflow. Best for power users, CI/CD pipelines, and version control.
GET /api/v1/custom-reports/:uid/export
POST /api/v1/custom-reports/import
A JRDL document is a JSON object with the following top-level keys:
| Key | Type | Required | Description |
|---|---|---|---|
version |
string | Yes |
Must be "1.0"
|
metadata |
object | No | Added automatically on export (name, description, created_by, created_at, workspace_origin) |
sources |
array | Yes | List of data source definitions (forms + field mappings) |
layout |
object | Yes |
Contains summary_cards, charts, and
tables
arrays
|
filters |
object | No | Default date range and filter toggles |
{
"version": "1.0",
"metadata": { ... }, // export only
"sources": [ ... ],
"layout": {
"summary_cards": [ ... ],
"charts": [ ... ],
"tables": [ ... ]
},
"filters": { ... }
}
Each source links a report to a form and defines how form fields map to report-level keys.
| Key | Type | Required | Description |
|---|---|---|---|
alias |
string | Yes | Unique identifier. Lowercase letters, digits, underscores. Must start with a letter. |
form_uid |
string | No* | Form UID for cross-workspace matching on import (added on export) |
form_name |
string | No* | Form name for fallback matching on import (added on export) |
form_id |
integer | No | Internal form ID. Set at runtime after linking; not required in JRDL files. |
field_mappings |
object | Yes |
Map of report_key → form_field_name.
Keys are used in layout components.
|
filters |
object | No | Per-source filters (reserved for future use) |
position |
integer | No | Ordering of sources (0-based) |
* For import: at least one of form_uid
or form_name
is needed for automatic form matching.
{
"alias": "intake",
"form_uid": "AZzVeQVRe3aLCwHt5yHaRA",
"form_name": "Patient Intake",
"field_mappings": {
"patient_name": "full_name",
"age": "patient_age",
"visit_date": "date_of_visit",
"department": "department"
},
"position": 0
}
Field Mappings: The left side (key) is the report key you reference in layout components. The right side (value) is the form field name as it appears in the form builder.
Summary cards display a single aggregated metric as a large number with a title, icon, and color.
| Key | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | Unique identifier for the card |
title |
string | Yes | Display title shown above the value |
source |
string | Yes | Source alias to pull data from |
aggregation |
string | Yes |
One of: count, sum, average, min, max,
distinct_count
|
field |
string | Conditional |
Report key to aggregate. Required for all aggregations except count.
|
color |
string | No |
One of: primary, secondary, accent, success, warning,
error
|
icon |
string | No |
Hero icon name (e.g., hero-users)
|
format |
string | No |
Value format: number, decimal, decimal:N, currency, percent,
k_suffix
|
{
"id": "card_avg_age",
"title": "Average Age",
"source": "intake",
"aggregation": "average",
"field": "age",
"color": "secondary",
"icon": "hero-user",
"format": "decimal:1"
}
Charts visualize data from a source. Each chart operates in one of two modes: distribution (categorical breakdown) or time series (temporal trend).
| Key | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | Unique identifier |
title |
string | Yes | Chart title |
source |
string | Yes | Source alias |
type |
string | Yes |
bar, line, doughnut, pie,
radar
|
width |
string | No |
full
(default), half,
third
|
group_by |
string | Conditional | Report key for distribution mode (categorical breakdown) |
x_axis |
object | Conditional | Time series X-axis config |
y_axis |
object | No | Y-axis aggregation for time series |
Groups data by a categorical field. Works with all chart types.
{
"id": "chart_dept",
"title": "Patients by Department",
"source": "intake",
"type": "doughnut",
"width": "half",
"group_by": "department"
}
Groups data by time periods. Best with line
or bar
charts.
| x_axis Key | Description |
|---|---|
field |
Date field to group by, or
_submitted_at
for submission date
|
group_by |
day, week, month, quarter,
year
|
| y_axis Key | Description |
|---|---|
aggregation |
count, sum, average, min,
max
|
field |
Report key to aggregate (required for non-count) |
{
"id": "chart_trend",
"title": "Patient Volume Over Time",
"source": "intake",
"type": "line",
"width": "full",
"x_axis": {
"field": "_submitted_at",
"group_by": "month"
},
"y_axis": {
"aggregation": "count"
}
}
Rule:
Each chart must have either group_by
(distribution) or x_axis
(time series), but not both.
Tables display individual submission rows with selected columns, sorting, and pagination.
| Key | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | Unique identifier |
title |
string | Yes | Table heading |
source |
string | Yes | Source alias |
columns |
array | Yes | List of report key strings to display as columns |
sort |
object | No |
field
(report key) + direction
(asc or desc)
|
limit |
integer | No | Max rows to display (default 100, max 1000) |
show_submission_link |
boolean | No | Show a link to the original submission (default false) |
{
"id": "table_recent",
"title": "Recent Patients",
"source": "intake",
"columns": ["patient_name", "age", "department", "visit_date"],
"sort": {
"field": "visit_date",
"direction": "desc"
},
"limit": 50,
"show_submission_link": true
}
The filters object configures date range defaults and which filter controls are available to users.
| Key | Type | Default | Description |
|---|---|---|---|
default_date_range |
string | "30d" |
One of: "7d", "30d", "90d",
"365d"
|
allow_date_range |
boolean | true | Show date range preset buttons (7D, 30D, 90D, 1Y) |
allow_custom_dates |
boolean | true | Show custom start/end date picker |
additional_filters |
array | [] |
List of objects with field
(report key) and label
(display text)
|
{
"default_date_range": "30d",
"allow_date_range": true,
"allow_custom_dates": true,
"additional_filters": [
{ "field": "department", "label": "Department" }
]
}
Every JRDL definition is validated before saving. The validator checks:
"1.0"
group_by
(distribution) or x_axis
(time series), not both
sum, average, min, max,
distinct_count
aggregations
base
or base:N
(e.g., "decimal:2")
Validation errors are returned as a list of
path
+ message
objects:
{
"errors": [
{ "path": "version", "message": "is required" },
{ "path": "sources[0].alias", "message": "must start with a letter and contain only lowercase letters, digits, underscores" },
{ "path": "layout.charts[0].type", "message": "must be one of: bar, line, doughnut, pie, radar" },
{ "path": "layout.summary_cards[1].source", "message": "references unknown source alias \"unknown_src\"" }
]
}
When you export a report, the JRDL definition is enriched with a
metadata
block and source form identifiers:
{
"metadata": {
"name": "Monthly KPI Dashboard",
"description": "Tracks key metrics across intake and feedback forms",
"created_by": "admin@example.com",
"created_at": "2025-06-01T12:00:00Z",
"workspace_origin": "My Workspace"
},
"sources": [
{
"alias": "intake",
"form_uid": "AZzVeQVRe3aLCwHt5yHaRA",
"form_name": "Patient Intake",
"field_mappings": { ... }
}
]
}
form_uid, then by
form_name
Returns an error with the list of missing form names if any source form cannot be found in the target workspace.
Pass allow_partial=true.
Imports the report with unmatched sources having
form_id=nil
— link them later in the builder.
JRDL definitions are workspace-agnostic. Source matching by
form_name
enables sharing between workspaces with similarly named forms. This makes JRDL ideal for:
| Function | Field Required | Description |
|---|---|---|
count |
No | Number of rows (submissions) in the source |
sum |
Yes | Sum of numeric field values |
average |
Yes | Mean of numeric field values |
min |
Yes | Minimum numeric value |
max |
Yes | Maximum numeric value |
distinct_count |
Yes | Count of unique non-nil values for a field |
Numeric Parsing:
Values are parsed as numbers by stripping $, %, and commas before conversion.
Both integers and floats are supported. Non-numeric values are silently skipped.
| Type | Modes | Description |
|---|---|---|
bar |
Distribution, Time Series | Vertical bars, great for comparisons |
line |
Time Series | Line with points and fill, tension 0.3. Best for trends over time. |
doughnut |
Distribution | Ring chart with multi-color segments for proportional breakdown |
pie |
Distribution | Full circle chart, similar to doughnut but filled |
radar |
Distribution | Multi-dimension comparison on a spider/web chart |
Chart
JavaScript hook
maintainAspectRatio: false
full
= 100%, half
= 50%, third
= 33%
The format
field on summary cards controls how the computed value is displayed.
| Format | Example Input | Example Output | Description |
|---|---|---|---|
number |
1234 | 1,234 | Comma-separated integer |
decimal |
3.14159 | 3.1 | Float rounded to 1 decimal |
decimal:N |
3.14159 | 3.14 | Float rounded to N decimals (e.g., decimal:2) |
currency |
1234.5 | $1,235 | "$" prefix + comma-separated integer |
percent |
85.3 | 85.3% | Value + "%" suffix |
k_suffix |
1200 | 1.2k | Abbreviated: k for thousands, M for millions |
null |
1234 | 1234 | Raw value, no formatting |
nil
values always display as —
regardless of format.
When a report is executed (viewed or exported), the engine processes data in this order:
submitted_at
falls between start_date
and end_date
"submitted"
and "approved"
status submissions are included
{
"summary_cards": [ { "id": "...", "title": "...", "value": 42 }, ... ],
"charts": [ { "id": "...", "title": "...", "chart_config": { ... } }, ... ],
"tables": [ { "id": "...", "title": "...", "rows": [...], "columns": [...] }, ... ],
"warnings": [ "Source 'feedback': form not found" ],
"source_data": { "intake": { "row_count": 150 } },
"date_filter_label": "Last 30 days"
}
All endpoints require a valid API token. See API Documentation for authentication details.
/api/v1/custom-reports
List custom reports visible to the token's user (own drafts + all published)
/api/v1/custom-reports/:uid
Execute and view report with results (summary cards, charts, tables)
Query params: start_date,
end_date
(YYYY-MM-DD)
/api/v1/custom-reports
Create a custom report (requires create_custom_reports permission)
/api/v1/custom-reports/:uid
Update a custom report (creator or owner only)
/api/v1/custom-reports/:uid
Soft-delete a custom report (creator or owner only)
/api/v1/custom-reports/:uid/export
Export report as a portable JRDL JSON file
/api/v1/custom-reports/import
Import a report from a JRDL JSON definition
Send as multipart/form-data
with the JSON as definition.
Pass allow_partial=true
to import even when some source forms are missing.
/api/v1/custom-reports/:uid/export/pdf
Download custom report as PDF
Query params: start_date,
end_date
(YYYY-MM-DD)
/api/v1/custom-reports/:uid/export/excel
Download custom report as Excel (.xlsx)
Query params: start_date,
end_date
(YYYY-MM-DD)
| Plan | Max Reports | Max Sources / Report | Max Charts / Report |
|---|---|---|---|
| Starter | 1 | 1 | 2 |
| Pro | 5 | 3 | 5 |
| Business | 20 | 5 | 10 |
| Enterprise | 50 | 10 | 20 |
| Unlimited | ∞ | ∞ | ∞ |
Free plan does not include custom reports. Upgrade to Starter or above to get started.
| Permission Key | Description | Default Roles |
|---|---|---|
create_custom_reports |
Create new custom reports | Owner, Manager |
edit_custom_reports |
Edit own custom reports | Owner, Manager |
edit_any_custom_report |
Edit any user's custom reports | Owner |
delete_custom_reports |
Delete own custom reports | Owner, Manager |
publish_custom_reports |
Publish reports visible to all members | Owner, Manager |
import_custom_reports |
Import JSON report definitions | Owner, Manager |
view_reports |
View published reports + own drafts | All members |
export_reports |
Export reports as PDF/Excel/JSON | Owner, Manager |
Permissions can be customized per role in Settings → Permissions. Owner always has all permissions.
A complete JRDL document with 2 sources, 3 summary cards, 2 charts, and 1 table:
{
"version": "1.0",
"sources": [
{
"alias": "intake",
"form_name": "Patient Intake",
"field_mappings": {
"patient_name": "full_name",
"age": "patient_age",
"visit_date": "date_of_visit",
"department": "department"
},
"position": 0
},
{
"alias": "feedback",
"form_name": "Patient Feedback",
"field_mappings": {
"rating": "satisfaction_rating",
"comment": "additional_comments"
},
"position": 1
}
],
"filters": {
"default_date_range": "30d",
"allow_date_range": true,
"allow_custom_dates": true
},
"layout": {
"summary_cards": [
{
"id": "card_total",
"title": "Total Patients",
"source": "intake",
"aggregation": "count",
"color": "primary",
"icon": "hero-users"
},
{
"id": "card_avg_age",
"title": "Average Age",
"source": "intake",
"aggregation": "average",
"field": "age",
"color": "secondary",
"format": "decimal:1"
},
{
"id": "card_avg_rating",
"title": "Avg Satisfaction",
"source": "feedback",
"aggregation": "average",
"field": "rating",
"color": "success",
"format": "decimal:1"
}
],
"charts": [
{
"id": "chart_trend",
"title": "Patient Volume Over Time",
"source": "intake",
"type": "line",
"width": "full",
"x_axis": {
"field": "_submitted_at",
"group_by": "month"
},
"y_axis": {
"aggregation": "count"
}
},
{
"id": "chart_dept",
"title": "Patients by Department",
"source": "intake",
"type": "doughnut",
"width": "half",
"group_by": "department"
}
],
"tables": [
{
"id": "table_recent",
"title": "Recent Patients",
"source": "intake",
"columns": ["patient_name", "age", "department", "visit_date"],
"sort": {
"field": "visit_date",
"direction": "desc"
},
"limit": 50,
"show_submission_link": true
}
]
}
}
| Term | Definition |
|---|---|
| JRDL | JSON Report Definition Language — the JSON schema for defining custom reports |
| Source | A link from a report to a form, with field mappings that translate form fields to report keys |
| Alias |
A short, unique identifier for a source within a report (e.g., intake, feedback)
|
| Field Mapping | A key-value pair mapping a report-level key to a form field name |
| Aggregation | A function applied to data rows: count, sum, average, min, max, distinct_count |
| Layout | The visual structure of a report: summary cards, charts, and tables |
| Summary Card | A single metric displayed as a large number with title and color |
| Distribution Chart | A chart that groups data by a categorical field (e.g., department) |
| Time Series Chart | A chart that groups data by time periods (day, week, month, quarter, year) |
| Report Key | The name used in layout components to reference data from a source's field mappings |
| Export Definition | A JRDL JSON file that captures the full report structure for portability |
| Partial Import | Importing a JRDL definition where some source forms don't exist in the target workspace |