Frappe Standard Script Report Schema
Companion to frappe-standard-script-report-controller (.py) and frappe-standard-script-report-view (.js). This skill covers the .json schema only.
Manipulate Script Report schemas through Frappe's document engine. Output is identical to a developer saving a Report in the Frappe UI in developer mode: Frappe exports canonical JSON and scaffolds missing .py/.js controller files.
Quick Start
# From frappe-bench root
env/bin/python <skill_dir>/scripts/save_report.py <path/to/report.json>
<skill_dir> = /workspace/development/frappe-bench/skills/.agents/skills/frappe-standard-script-report-schema
No bench migrate required — reports do not change database schema.
Workflow
New Report
- Write desired-state JSON (see skeleton below) at any writable path
- Run
save_report.py— Frappe inserts the doc, exports canonical JSON, scaffolds.pyand.js - The
.pystub (execute(filters=None)) and.jsstub (frappe.query_reports["Name"] = { filters: [] }) are written only if they do not already exist
Canonical output directory (printed by script):
apps/<app>/<app>/<module_scrubbed>/report/<report_name_scrubbed>/
Editing an Existing Report
- Read the existing
.jsonwithread_fileto get current state - Build complete desired-state JSON (full replace — every field must be present)
- Run
save_report.py— Frappe updates the doc and re-exports JSON - Existing
.pyand.jsare not touched — only missing files are scaffolded
Minimal Valid JSON
{
"doctype": "Report",
"report_name": "My Sales Summary",
"ref_doctype": "Sales Invoice",
"report_type": "Script Report",
"is_standard": "Yes",
"module": "Accounts",
"add_total_row": 0,
"disabled": 0,
"prepared_report": 1,
"roles": [
{ "role": "System Manager" }
]
}
nameis auto-derived fromreport_name(autoname rule:field:report_name). You may include"name": "My Sales Summary"for clarity — it must matchreport_name.
Key Fields
| Field | Type | Notes |
|---|---|---|
report_name |
Data | Primary key. Must be unique. |
ref_doctype |
Link → DocType | Required. The doctype this report is "about". Drives default roles. |
module |
Link → Module Def | Always specify. Determines where files are exported. |
is_standard |
"Yes" |
Required for file-backed reports. "No" = custom report stored only in DB. |
report_type |
"Script Report" |
Fixed for this skill. |
add_total_row |
0 / 1 | Adds a totals row at the bottom. |
disabled |
0 / 1 | Hides the report from menus. |
prepared_report |
0 / 1 | Preferred: always set to 1. Runs the report async and caches results; prevents timeouts on heavy queries. |
timeout |
Int | Custom timeout in seconds (only meaningful when prepared_report = 1). |
roles |
Table (see below) | Who can run the report. Empty = all users. |
roles Child Table
Each entry is a Has Role row:
"roles": [
{ "role": "Accounts User" },
{ "role": "Stock User" }
]
Filters → always in .js, never in JSON
Do not add a filters child table to Script Report JSON. Frappe reads filter definitions from frappe.query_reports["Name"].filters in the on-disk .js file at runtime. Define all filters there.
Columns → always returned from .py, never in JSON
Do not add a columns child table to Script Report JSON. Script Reports always return columns dynamically from execute(). Declare them in the Python return value, not in the schema.
DO NOT Set in Agent-Authored JSON
creation,modified,modified_by,owner— managed by Frappe on savefilters— always in the.jsfile, never in the JSON schemacolumns— always returned fromexecute()in.py, never in the JSON schemaquery— only for"report_type": "Query Report"report_script— only for non-standard (is_standard: "No") Script Reports stored in DBjavascript— only for non-standard Script Reports stored in DB; for standard reports, the JS lives in the on-disk.jsfile, not in the schema
On-Disk File Layout
After running save_report.py, the report folder contains:
apps/<app>/<app>/<module_scrubbed>/report/<name_scrubbed>/
├── <name_scrubbed>.json ← canonical schema (managed by Frappe, do not hand-edit)
├── <name_scrubbed>.py ← execute() implementation (scaffolded once, then yours to edit)
└── <name_scrubbed>.js ← frappe.query_reports["…"] filter definitions (scaffolded once)
The .py and .js are only written when the file is absent. Re-running save_report.py on an existing report never overwrites them.
.py Stub (auto-scaffolded)
# Copyright (c) {year}, {publisher} and contributors
# For license information, please see license.txt
# import frappe
def execute(filters=None):
columns, data = [], []
return columns, data
execute() must return (columns, data):
columns— list of column dicts or"fieldname:Label:width"stringsdata— list of row dicts (keys matching columnfieldnamevalues)
.js Stub (auto-scaffolded)
// Copyright (c) {year}, {publisher} and contributors
// For license information, please see license.txt
frappe.query_reports["My Sales Summary"] = {
"filters": [
// add filter objects here
]
};
Filter objects use the same shape as standard Frappe filter field dicts (same fields as Report Filter child table).