Grafana OSS has no native PDF reporting, so getting a scheduled dashboard report to a client means building the pipeline yourself: grafana-image-renderer for the panels, a script to stitch the PNGs into a PDF, cron to fire it, and SMTP to send it. It works until it doesn’t, and it usually breaks at the seams between those parts, not in the parts themselves.
The alternative is to not own the chain at all. The Grafana report script below triggers a pre-designed (customisable) PDF report with one POST request to Skedler, which handles rendering, assembly, scheduling, and delivery after exporting a Grafana dashboard to PDF.
How to build a Grafana PDF report script (and where it breaks)
Here is the full pipeline: install and run grafana-image-renderer (its own service, with its own memory appetite), call the render API per panel with a service account token, collect the PNGs, assemble them into a PDF with a library like ReportLab or wkhtmltopdf, schedule it with cron, and send the result through SMTP code you also wrote.
Five things to keep alive. Each connection between them is a place to fail.
That is where it breaks. The render API authenticates with a token that will eventually rotate. The renderer breaks in new ways after Grafana version upgrades. Template variables that resolve fine in your browser come back empty in a headless render. And when a report silently fails to send, there is no alert, because nothing monitors the monitor.
Keeping in mind these unexpected failures we engineered a single API call below that removes the seams by removing the components. One call does the rendering, assembly, and delivery. But before the call can work, it needs one thing to point at. That is the setup the next section covers.
How to create a Grafana report to trigger via API
The script triggers a report, so a report has to exist first, making this one-time setup is deliberate.
Every team’s reporting requirement is different, so this step is yours to control rather than something the tool forces on you. Instead of configuring a fixed template you have to live with, you can lay out exactly how the client-facing PDF should look. This means your branding, your logo, pagination, white-labeled, and the panels in the order that makes sense for the reader. Importantly, this reflects reader needs rather than the order they sit on the dashboard.
Start with these two guides:
- Install Skedler Reports: https://support.skedler.com/support/solutions/articles/8000096617-install-skedler-reports
- Design a Grafana report: https://support.skedler.com/support/solutions/articles/8000096646
This design step is the one thing a script will never do for you, and it is worth being clear about why. A render script can only ever hand you what the render API returns: panel screenshots. You can stitch them, but you cannot make code invent a cover page. Likewise, code won’t hold consistent branding across pages, paginate cleanly when the content grows, or white-label the output for a client who should never see the word “Grafana.” Those are layout and design decisions, not API calls. No amount of Python closes that gap, because the gap is not technical; the raw materials simply are not there.
That is the real line between the two approaches. A script gets you a PDF of your dashboard. The design step gets you a document your client opens without knowing how it was made. You do it once, and from then on the script reproduces that exact client-ready report on demand.
Once the report is designed and saved, it has a Report ID, and from that point everything is scriptable. You design once; you trigger forever.
With the report designed, the script needs four values to run:
1. Your Skedler server URL and port
2. An authorization token
3. Your account ID
4. Report ID.
Below is the script and right after it is exactly where to find each of those four.
Grafana report script: Python and curl
Here is the Grafana report script in both curl and Python. It connects to Skedler, which renders the report server-side and delivers it through distribution channel (Email, Slack, AWS S3 buckets) the report is configured with. Copy one of the scripts, fill in the four values, and just run it.
Generate a Grafana report with curl
curl -X POST "http://:/api/1/report/generateReport?notificationType=generate" \
-H "Authorization: " \
-H "AccountId: sks-tenant" \
-H "Content-Type: application/json" \
-d '{
"reportDetails": [""]
}'
Generate a Grafana report with Python Script
import requests
SKEDLER_URL = "http://localhost:3005" # your Skedler server URL and port
AUTH_TOKEN = "" # see "How to find your token, account ID, and report ID" below
ACCOUNT_ID = "sks-tenant" # your tenant ID
REPORT_ID = "RE-yjFp1NT80" # the report to generate
def generate_report(report_id: str) -> None:
response = requests.post(
f"{SKEDLER_URL}/api/1/report/generateReport",
params={"notificationType": "generate"},
headers={
"Authorization": AUTH_TOKEN,
"AccountId": ACCOUNT_ID,
"Content-Type": "application/json",
},
json={"reportDetails": [report_id]},
timeout=60,
)
response.raise_for_status()
print(f"Report {report_id} triggered: {response.status_code}")
if __name__ == "__main__":
generate_report(REPORT_ID)
That is the whole script. No renderer process to babysit, no PNG stitching, no SMTP block. Rendering, layout, and delivery happen on the Skedler side, and if a report fails, Skedler alerts you via email with a reason and logs instead of failing silently.
Run this on your own Grafana
Get a free Skedler POV key and trigger your first report.
Get My LicenseHow to find your Skedler token, account ID, and report ID
You find all four values in the Skedler web UI. Two of them are available through your browser’s developer tools. Here is where each one lives.
Report ID. Open the Skedler web UI, go to the Reports list, click Edit on your report, and read the browser URL. The value after edit= is the Report ID, for example RE-yjFp1NT80.
Authorization token and Account ID. In the Skedler web UI, open your browser developer tools (right-click, Inspect), go to the Network tab, and reload the page. Select the request named current and look under Headers: Authorization holds the token and AccountId holds the tenant ID.
Server URL and port. Wherever your Skedler instance runs, for example http://localhost:3005.
Keep the token out of your repo. Load it from an environment variable or your secrets manager. This is the same way you would treat a Grafana service account token.

Grafana report script: best practices
- Make sure the Skedler server is running and reachable from wherever the script runs.
- Confirm the Grafana dashboards linked to the report load properly before you trigger it.
- Test the call with Postman or curl first, then drop it into your script or scheduler.
- Keep your authorization token secure and never commit it or expose it publicly.
How to script multi-tenant Grafana reports from one dashboard
You send per-client reports by defining a filter per segment, so one dashboard fans out into separate branded PDFs for each recipient. If your task is not one report but one report per client (each filtered to their own data), Skedler’s filter API covers it: you define queries and recipient lists per segment.
{
"name": "Filter-1",
"filters": [
{
"name": "query-1",
"filter": "OriginCountry: AE | OriginCityName: Dubai",
"emaillist": ["[email protected]"]
},
{
"name": "query-2",
"filter": "OriginCountry: US | OriginCityName: New York",
"emaillist": ["[email protected]"]
}
],
"orgId": "",
"userId": ""
}
POST that to /api/1/filter/ with the same headers, and update existing filters with a PUT to /api/1/filter/. If you have ever tried to do per-client filtering with the raw render API and URL parameters, you know how much script this replaces.
When should you build your own Grafana report script instead?
Build it yourself when the report is a one-off, when you only need a single panel dropped into a Slack alert, or when owning the pipeline is genuinely worth your time. The render API is documented. The DIY route costs nothing but hours.
The script above earns its place the moment reporting becomes routine: recurring, client-facing, or one per customer. That is when a broken seam stops costing you an evening and starts costing you a client’s trust, and it is also the point where Grafana’s own answer is an Enterprise license, a far bigger commitment than Skedler.
Want it working without the setup?
We connect Grafana and build your first report for you.
Book guided setupGrafana report script FAQs
Not natively. Grafana OSS has no built-in PDF reporting, so you either script the image renderer and assemble PDFs yourself, or call a reporting API. Skedler exposes a REST API that generates and delivers a finished, branded PDF report in a single request, which is what the script in this post uses.
Trigger them through a reporting API instead of rendering panels yourself. Skedler renders the full report server-side and returns a paginated PDF, so your script makes one POST call and never touches grafana-image-renderer, Puppeteer, or PDF-assembly libraries.
Because variables that resolve in your browser session are often empty in a headless render, where there is no interactive context to populate them. Skedler resolves variables inside its own rendering, so a scheduled report produces the same output you see on screen, which is one of the main reasons teams move off a DIY render script.
Define a filter per client so one dashboard fans out into separate branded PDFs, each scoped to that client’s data and sent to their own recipients. Skedler’s filter API handles this, which is what makes per-customer and multi-tenant reporting work without rebuilding a template per client.
Yes. The Skedler API call is a normal POST, so any scheduler triggers it: cron, n8n, Airflow, or a CI job. Skedler also has a built-in scheduler if you would rather not run one at all.
Use a reporting layer that alerts on failure instead of a chain of scripts that fail quietly. With a DIY pipeline a missed report surfaces only when the client asks; Skedler reports through a monitored delivery path, so a failure raises an alert rather than going unnoticed.
Yes. Skedler offers a 15-day free trial with no credit card required, and you activate it with a license key requested through support email ([email protected]) The script in this post runs on the trial, so you can test the full flow end to end before deciding.


