JRDL Documentation

JSON Report Definition Language v1.0 — Custom Reports Schema Reference

Introduction

What is JRDL?

JRDL (JSON Report Definition Language) is a declarative JSON format for defining custom data reports in Papereg. A single JRDL document fully describes:

  • Data sources — which forms to pull data from, with field mappings that translate form field names to report-level keys
  • Layout components — summary cards, charts, and tables that visualize the data
  • Filter configuration — default date ranges and user-configurable filter options

JRDL is the portable format used for exporting and importing custom reports between workspaces.

What are Custom Reports?

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.

Relationship to Preset Reports

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".

Quick Start

Minimal Example

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"
}
}

Builder vs JSON

There are two ways to create custom reports:

Visual Builder

A 5-step wizard in the UI: Info → Sources → Layout → Filters → Review. No JSON required. Best for most users.

Raw JRDL JSON

Write or edit JRDL directly via the API or the Export → Edit → Import workflow. Best for power users, CI/CD pipelines, and version control.

Export → Edit → Import

  1. Create a report using the visual builder
  2. Export it as JSON via the UI or GET /api/v1/custom-reports/:uid/export
  3. Edit the JSON file with your preferred editor
  4. Import back via the UI or POST /api/v1/custom-reports/import

Schema Reference

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": { ... }
}

Sources

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

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

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

Distribution Mode

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"
}

Time Series Mode

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

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
}

Filters

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" }
]
}

Validation Rules

Every JRDL definition is validated before saving. The validator checks:

  • Version must be "1.0"
  • Source aliases must be unique, start with a letter, and contain only lowercase letters, digits, and underscores
  • Layout component sources must reference a defined source alias
  • Charts must have either group_by (distribution) or x_axis (time series), not both
  • Field required for sum, average, min, max, distinct_count aggregations
  • Format strings must match pattern: base or base:N (e.g., "decimal:2")
  • Table limit must be a positive integer, max 1000

Error Format

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\"" }
]
}

Export / Import

Export Format

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": { ... }
}
]
}

Import Flow

  1. Parse JSON and validate against the JRDL v1.0 schema
  2. Match sources to forms: first by form_uid, then by form_name
  3. Handle missing forms based on import mode
  4. Create report as draft (not published) with " (Imported)" suffix

Strict Mode (default)

Returns an error with the list of missing form names if any source form cannot be found in the target workspace.

Partial Mode

Pass allow_partial=true. Imports the report with unmatched sources having form_id=nil — link them later in the builder.

Cross-Workspace Portability

JRDL definitions are workspace-agnostic. Source matching by form_name enables sharing between workspaces with similarly named forms. This makes JRDL ideal for:

  • Sharing report templates across teams or organizations
  • Bundling reports with workspace presets
  • Version-controlling report definitions in Git

Aggregation Functions

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.

Chart Types & Rendering

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

Rendering Details

  • Chart.js config is generated server-side and rendered via the Chart JavaScript hook
  • All charts are responsive with maintainAspectRatio: false
  • Charts automatically adapt to light/dark theme
  • Width values control the grid: full = 100%, half = 50%, third = 33%

Value Formatting

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.

Execution Pipeline

When a report is executed (viewed or exported), the engine processes data in this order:

  1. Source Loading — For each source, look up the form by ID, resolve field mappings to field IDs, and query submissions
  2. Date Filtering — Filter submissions where submitted_at falls between start_date and end_date
  3. Status Filtering — Only "submitted" and "approved" status submissions are included
  4. Field Mapping — Transform each row using: report_key → form_field_name → field_id → submission_data value
  5. Summary Card Computation — Apply aggregation functions to the mapped data per source
  6. Chart Building — Group data and build Chart.js-compatible config objects
  7. Table Building — Select columns, apply sorting and limits
  8. Warning Collection — Collect warnings for missing forms, missing fields, and non-numeric aggregation attempts

Result Structure

{
"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"
}

API Endpoints

All endpoints require a valid API token. See API Documentation for authentication details.

GET /api/v1/custom-reports

List custom reports visible to the token's user (own drafts + all published)

GET /api/v1/custom-reports/:uid

Execute and view report with results (summary cards, charts, tables)

Query params: start_date, end_date (YYYY-MM-DD)

POST /api/v1/custom-reports

Create a custom report (requires create_custom_reports permission)

PUT /api/v1/custom-reports/:uid

Update a custom report (creator or owner only)

DELETE /api/v1/custom-reports/:uid

Soft-delete a custom report (creator or owner only)

GET /api/v1/custom-reports/:uid/export

Export report as a portable JRDL JSON file

POST /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.

GET /api/v1/custom-reports/:uid/export/pdf

Download custom report as PDF

Query params: start_date, end_date (YYYY-MM-DD)

GET /api/v1/custom-reports/:uid/export/excel

Download custom report as Excel (.xlsx)

Query params: start_date, end_date (YYYY-MM-DD)

Plan Limits

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.

Permissions

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.

Full Example

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
  }
]
}
}

Glossary

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