Create your first Scripted Report

Create your first Scripted Report

A step-by-step walkthrough: from an empty report to a working time-tracking report built with HTML and JavaScript. You need basic HTML and JavaScript. You do not need to know Jira's REST API — the data arrives ready, and the API pages describe the shape it arrives in rather than how to fetch it.

Before you start

Report Builder covers most reporting needs with its visual builder. Scripted Reports exist for the cases the visual builder cannot express — a custom invoice, a diagram, an export in someone else's format, a layout no pivot table can produce. If a Universal Visual Builder report would do the job, build that instead: it is quicker to make and much easier for the next person to maintain.

Make sure your app administrator has granted you the permission to create Scripted Reports.

Note for administrators. Grant the Scripted Reports creation permission only to users with solid JavaScript knowledge and only to people you trust. Report code runs in your users' browsers with their Jira session; careless code can corrupt data, and hostile code can be used for XSS or other injection attacks.

How a report is put together

Every Scripted Report follows the same four steps, and the editor has a tab for each of them:

  1. Declare input parameters — scope, dates, a JQL filter, whatever the user should choose. (Fields tab)

  2. Fetch raw data based on those parameters: issues, projects, worklogs. (Script tab)

  3. Apply your algorithm to turn the raw data into the numbers you want. (Script tab)

  4. Format the result as a table, a chart or anything else HTML can express. (Template tab)

Step 1. Create the report

Open Report Builder, click New report, and pick Blank (custom code) from the Templates list.

Templates list with Blank (custom code) as the second entry
New report → Templates → Blank (custom code).

Good news: the blank template is not blank. It ships with a complete, heavily commented version of the very report this page builds — summary cards, a per-project table and a per-issue table. You can follow this tutorial from scratch, or open the Script tab and read the shipped code first. Either way, rename the report before you save it.

Step 2. Declare the input parameters

Go to the Fields tab. Our report needs two parameters:

  • range, a Range Picker — the period we report on.

  • jql-field, a Jql field — an optional extra filter, with syntax checking built in.

Fields tab with the range and jql-field parameters
Fields tab. Name is what your code refers to; Title is what the user reads.

To add a field, pick its type in the Field Type selector and press +. The pencil icon opens Edit Field, where you set the two names:

Edit Field dialog
Field is the identifier used in code. Field Title is the label in the Configure panel.

Twenty field types are available, from plain String and Checkbox to Project, User Picker, Sprint Picker and Issue Picker. Choose the most specific one that fits: a Project picker gives you validated project keys, where a String field gives you whatever the user typed.

Assume every field can be empty. Parameter validation belongs at the very top of your script, before the first Jira call.

Step 3. Write the template

The Template tab holds HTML, CSS and Handlebars expressions. We expect an array of issue objects, so we loop over it and produce one table row each.

Template tab with the HTML editor
Template tab. Beautify, top right, reformats the code.
<div class="issues-container"> <h2>Issues: work vs original estimate</h2> <table> <tr> <th>Ticket</th> <th>Actual work</th> <th>Original estimate</th> <th>Remaining</th> </tr> {{#each issues}} <tr> <td>{{this.key}}</td> <td>{{this.timespent}} h</td> <td>{{this.originalEstimate}} h</td> <td>{{this.remaining}} h</td> </tr> {{/each}} </table> </div>

Step 4. Write the script

Script tab with the JavaScript editor
Script tab. The shipped tutorial script is commented step by step.

The code runs in a top-level await scope with the SR object available, so asynchronous calls read like synchronous ones. The full API reference lives at sr-docs and on the Scripted Reports API page. Three methods carry this tutorial:

  • SR.fields.getValueByFieldName(fieldName) — returns a promise with the value of an input field declared on the Fields tab. Multi-select pickers return an array.

  • SR.jira.getIssuesByJQL(jql, fields, expand) — returns a promise with an array of Jira issues. If fields is omitted you get the navigable fields.

  • SR.render(values, callback) — passes values to the template. The optional callback runs after the HTML has been rendered, which is where DOM work belongs.

Read the parameters first:

const jqlField = await SR.fields.getValueByFieldName('jql-field'); const { start, end } = await SR.fields.getValueByFieldName('range');

Then build the JQL string and fetch the issues. Both parameters of getIssuesByJQL() are strings, so the field list is joined with commas:

const jql = `created >= "${start}" AND created <= "${end}"${jqlField ? ` AND ${jqlField}` : ''}`; const foundIssues = await SR.jira.getIssuesByJQL(jql, FIELDS.join(','));

Jira returns time-tracking values in seconds, not in hours. Convert them once, in a helper, rather than in the template.

Finally, shape the response into exactly what the template expects and hand it over. Here is the whole script:

/* 1. Which Jira fields we need */ const FIELDS = [ 'timespent', 'timeoriginalestimate', 'timeestimate' ]; /* 2. Jira gives time tracking values in seconds */ const secondsToHours = (seconds) => { if (isNaN(seconds)) { return 0; } return Math.round(((seconds / 3600) + Number.EPSILON) * 100) / 100; }; /* 3. Read the input parameters declared on the Fields tab */ const jqlField = await SR.fields.getValueByFieldName('jql-field'); const { start, end } = await SR.fields.getValueByFieldName('range'); /* 4. Build the JQL and fetch the issues */ const jql = `created >= "${start}" AND created <= "${end}"${jqlField ? ` AND ${jqlField}` : ''}`; const foundIssues = await SR.jira.getIssuesByJQL(jql, FIELDS.join(',')); /* 5. Shape the response into what the template expects */ const issues = foundIssues.map(({ key, fields }) => ({ key, timespent: secondsToHours(fields.timespent), originalEstimate: secondsToHours(fields.timeoriginalestimate), remaining: secondsToHours(fields.timeestimate) })); /* 6. Hand the data to the Handlebars template */ SR.render({ issues });

Step 5. Run it

Switch to the Preview tab, fill the parameters in the floating Configure panel, and press Refresh next to the report title.

Preview tab with the rendered report and the Configure panel
Preview tab. Every change to a parameter, the template or the script needs a Refresh to take effect.

Step 6. Share it

The Permissions tab decides who else can use the report. Pick an access level, pick the audience, press Add.

Permissions tab
Permissions tab. Shared reports appear in the recipient's Shared tab.
  • View — open and run the report; cannot save parameter values or edit the configuration.

  • Edit — the above, plus saving parameter values.

  • Admin — full access, including the editor tabs.

The creator always keeps admin rights, and that cannot be changed.

Step 7. Save it

Give the report a self-descriptive name in the title field, then press Add to My Reports. It now lives in the My tab of the report list, and opens with a single click.

My tab of the report list with the saved report selected
The saved report at the top of the My list, running.

Be deliberate about names. Once you have a dozen reports it is tempting to call the second one "Project Status" as well. Call it "Project Delivery Status" instead — your future self will thank you.

Going further: aggregate by project

The report now compares spent and estimated time per issue. Rolling that up per project takes only a change in the script. First, add project to the field list:

const FIELDS = [ 'timespent', 'timeoriginalestimate', 'timeestimate', 'project' ];

Then replace the map() with a reduce() that builds both collections in one pass:

const { issues, projects } = foundIssues.reduce((result, { key, fields }) => { const { project: { key: projectKey }, timeestimate, timeoriginalestimate, timespent } = fields; /* the per-issue row, exactly as before */ result.issues.push({ key, timespent: secondsToHours(timespent), originalEstimate: secondsToHours(timeoriginalestimate), remaining: secondsToHours(timeestimate) }); /* the per-project totals, accumulated in seconds */ let project = result.projects.find((p) => p.key === projectKey); if (!project) { project = { key: projectKey, timespent: 0, originalEstimate: 0, remaining: 0 }; result.projects.push(project); } project.timespent += timespent || 0; project.originalEstimate += timeoriginalestimate || 0; project.remaining += timeestimate || 0; return result; }, { issues: [], projects: [] }); /* convert the project totals to hours once, at the end */ projects.forEach((p) => { p.timespent = secondsToHours(p.timespent); p.originalEstimate = secondsToHours(p.originalEstimate); p.remaining = secondsToHours(p.remaining); }); SR.render({ issues, projects });

And add a second table to the template:

<div class="projects-container"> <h2>Projects: work vs original estimate</h2> <table> <tr> <th>Project key</th> <th>Actual work</th> <th>Original estimate</th> <th>Remaining</th> </tr> {{#each projects}} <tr> <td>{{this.key}}</td> <td>{{this.timespent}} h</td> <td>{{this.originalEstimate}} h</td> <td>{{this.remaining}} h</td> </tr> {{/each}} </table> </div>

Note that you did not lose the first table — the same fetched data now feeds two views. That is the point of Scripted Reports: the Jira data arrives once, and what you do with it is up to your code.

Moving a report between instances

A Scripted Report is a piece of JSON, so it travels. Open the … menu next to the report title and choose Export JSON.

The three-dot menu with Save as, Print, Export To PDF and Export JSON
The same menu also holds Save as… for duplicating a report and Export To PDF (as Image) for the rendered result.

On the target instance, click New report, scroll to the bottom of the Templates list and pick Import Scripted Report. Paste the JSON and press Import.

JSON Import dialog opened from the Import Scripted Report template card
Import Scripted Report, at the bottom of the Templates list, opens the JSON Import dialog.

Details and edge cases: Report Builder: "How to export and import a Scripted Report" guide

Report Building Service

If you would rather have the report built for you, our Report Building Service team can do it. Read more here.