Automating Jira Service Management Reporting with Power Automate
Generating recurring Jira Service Management (JSM) reports can be labor-intensive, especially when dealing with pagination, custom fields, and CSV formatting requirements. This guide provides a step-by-step walkthrough for building an automated reporting workflow using Power Automate, Jira Cloud API, and SharePoint.
This article is written for IT professionals who want to implement this solution without exposing sensitive organizational details. All examples use sanitized placeholders that you can replace with your own values.
Architecture Overview
What We Are Going to Build
A Power Automate flow that:
- Runs on a fixed schedule (e.g., every 2 weeks)
- Calls the Jira Cloud REST API with proper pagination handling
- Collects all issues from a saved Jira filter (even if there are 500+ issues)
- Flattens nested JSON fields (including custom fields) into a CSV file
- Saves the CSV to SharePoint
- Sends an email with the CSV attached
Demo & Live Example
Want to see what this automation can achieve?
Check out the final result showcase with real-world dashboards and KPI visualizations:
JSM Sales Performance Dashboard - GitHub Repository
This repository showcases:
- KPI dashboards built from automated Jira reports
- Real-world use case: Sales team performance tracking
- Data visualization examples: How CSV reports become actionable insights
- Production deployment: Live implementation in enterprise environment
Prerequisites
Before starting, ensure you have:
- Jira Cloud with:
- A saved filter (we will use its filter ID)
- A Jira API token for authentication
- Microsoft 365 with:
- Power Automate access
- SharePoint Online
- Outlook mailbox (for email delivery)
Step 1: Get Your Jira Filter ID
How to Find the Filter ID
-
Open Jira in your browser
-
Go to Filters → View all filters
-
Click the filter you want to use for the report
-
Look at the browser URL:
https://yourtenant.atlassian.net/issues/?filter=12345 -
The number after
filter=is your filter ID (in this example:12345)
In the API request body, you will use:
"jql": "filter = 12345"
Replace 12345 with your actual filter ID.
Step 2: Generate a Jira API Token
- Go to https://id.atlassian.com/manage-profile/security/api-tokens
- Click Create API token
- Give it a label (e.g., "Power Automate Integration")
- Copy the token and save it securely
- You will use this token as the password in HTTP Basic Authentication
Step 3: Understanding the Jira Search API
API Endpoint
POST https://<YOUR_TENANT>.atlassian.net/rest/api/3/search/jql
Request Body (Example)
{
"jql": "filter = 12345",
"maxResults": 100,
"fields": [
"key",
"summary",
"status",
"duedate",
"resolution",
"created",
"customfield_10001",
"customfield_10002",
"customfield_10003",
"assignee",
"reporter",
"priority",
"description"
],
"nextPageToken": ""
}
Key Parameters
| Parameter | Description |
|---|---|
jql | Your JQL query (using filter ID is recommended) |
maxResults | Number of issues per page (recommended: 100) |
fields | Array of field IDs to retrieve (include customfield_XXXXX) |
nextPageToken | Empty string for first call, then use value from response |
API Response (Simplified)
{
"issues": [ /* array of issue objects */ ],
"isLast": false,
"nextPageToken": "eyJwYWdlIjoyfQ=="
}
Critical fields:
issues- Array of issue objects (up to 100 per page)isLast- Boolean indicating if this is the last pagenextPageToken- Token to fetch the next page
Step 4: Build the Power Automate Flow
Power Automate Flow Architecture
Before we start building, here's the complete flow structure:
Flow Sections:
| Section | Color | Actions | Purpose |
|---|---|---|---|
| Initialization | Blue | Recurrence + 3 Variables | Set up trigger and storage |
| Pagination Loop | Yellow | HTTP + Parse + Append + Update | Collect all issues across pages |
| Data Transformation | Green | Select + CSV | Flatten JSON to CSV |
| Output | Purple/Red | SharePoint + Email | Deliver results |
Key Points:
- Loop structure: Continues until
isLast = truefrom Jira API - Variable updates: Happen inside the loop after each page
- Parallel outputs: CSV goes to both SharePoint and Email simultaneously
- Safety limits: Loop has 60 iteration max and 1-hour timeout
4.1 Create a New Scheduled Flow
- Go to Power Automate
- Click Create → Scheduled cloud flow
- Configure:
- Flow name:
Jira Biweekly CSV Report - Starting: Select your start date/time
- Repeat every:
2→ Weeks
- Flow name:
- Click Create
4.2 Configure the Recurrence Trigger
Set these values in the Recurrence trigger:
| Property | Value |
|---|---|
| Frequency | Week |
| Interval | 2 |
| Time zone | Your timezone (e.g., (UTC+08:00) Taipei) |
| At these hours | 9 |
| At these minutes | 0 |
This means: Run every 2 weeks at 09:00
4.3 Initialize Variables
Add three Initialize variable actions:
Variable 1: varAllIssues (Array)
- Name:
varAllIssues - Type:
Array - Value: (leave empty)
Variable 2: varNextPageToken (String)
- Name:
varNextPageToken - Type:
String - Value: (leave empty - this represents
"")
Variable 3: varIsLast (Boolean)
- Name:
varIsLast - Type:
Boolean - Value:
false
4.4 Add a "Do until" Loop
Add action: Do until
Condition configuration:
- Left side:
variables('varIsLast') - Operator:
is equal to - Right side:
true
Change limits:
- Count:
60(maximum 60 iterations) - Timeout:
PT1H(1 hour)
This ensures the loop will:
- Continue until Jira indicates the last page (
isLast = true) - Have safety limits to prevent infinite loops
4.5 Inside "Do until" - HTTP Call to Jira
Action: HTTP
| Property | Value |
|---|---|
| Method | POST |
| URI | https://<YOUR_TENANT>.atlassian.net/rest/api/3/search/jql |
| Headers | Content-Type: application/json |
| Authentication | Basic |
| Username | Your Jira account email |
| Password | Your Jira API token (from Step 2) |
Body (use code view for easier editing):
{
"jql": "filter = 12345",
"maxResults": 100,
"fields": [
"key",
"summary",
"status",
"duedate",
"resolution",
"created",
"customfield_10001",
"customfield_10002",
"customfield_10003",
"assignee",
"reporter",
"priority",
"description"
],
"nextPageToken": "@{variables('varNextPageToken')}"
}
Important notes:
- Replace
<YOUR_TENANT>with your Jira tenant name - Replace
12345with your actual filter ID - Replace
customfield_XXXXXwith your actual custom field IDs - The
@{variables('varNextPageToken')}expression will be evaluated by Power Automate
4.6 Inside "Do until" - Parse JSON
Add action: Parse JSON
| Property | Value |
|---|---|
| Content | Dynamic content: Body (from HTTP step) |
| Schema | See schema below |
JSON Schema (example):
{
"type": "object",
"properties": {
"issues": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"key": { "type": "string" },
"fields": { "type": "object" }
},
"required": ["id", "key", "fields"]
}
},
"nextPageToken": {
"type": ["string", "null"]
},
"isLast": {
"type": "boolean"
}
},
"required": ["issues", "isLast"]
}
Tip: You can generate this schema automatically:
- Run your HTTP call once manually (using Postman or similar)
- Copy the response JSON
- In Power Automate, click Use sample payload to generate schema
- Paste your sample response
4.7 Inside "Do until" - Append Each Issue
Add action: Apply to each
| Property | Value |
|---|---|
| Select an output from previous steps | body('Parse_JSON')?['issues'] |
Inside this Apply to each, add action: Append to array variable
| Property | Value |
|---|---|
| Name | varAllIssues |
| Value | item() (expression) |
Critical: Do NOT use union()
Never use:
union(variables('varAllIssues'), variables('varAllIssues'))
This causes a self-reference error. Always use Append to array variable.
4.8 Inside "Do until" - Update Pagination Variables
After the Apply to each (still inside the Do until loop), add two Set variable actions:
Set Variable 1: varNextPageToken
| Property | Value |
|---|---|
| Name | varNextPageToken |
| Value | body('Parse_JSON')?['nextPageToken'] |
Set Variable 2: varIsLast
| Property | Value |
|---|---|
| Name | varIsLast |
| Value | body('Parse_JSON')?['isLast'] |
Critical: Never hardcode true
Always use the API response value, not a hardcoded boolean. This ensures proper pagination handling.
Step 5: Flatten Issues to CSV Format
5.1 Select Action (Field Mapping)
After the Do until loop (outside of it), add action: Select
| Property | Value |
|---|---|
| From | variables('varAllIssues') |
Field mappings (add each row via GUI):
| Column Name | Value (Expression) |
|---|---|
| IssueKey | item()?['key'] |
| IssueId | item()?['id'] |
| Summary | item()?['fields']?['summary'] |
| Status | item()?['fields']?['status']?['name'] |
| DueDate | item()?['fields']?['duedate'] |
| Resolution | item()?['fields']?['resolution']?['name'] |
| Created | item()?['fields']?['created'] |
| CustomField1 | item()?['fields']?['customfield_10001']?['value'] |
| CustomField2 | item()?['fields']?['customfield_10002'] |
| CustomField3 | item()?['fields']?['customfield_10003']?['displayName'] |
| AssigneeName | item()?['fields']?['assignee']?['displayName'] |
| AssigneeEmail | item()?['fields']?['assignee']?['emailAddress'] |
| ReporterName | item()?['fields']?['reporter']?['displayName'] |
| Priority | item()?['fields']?['priority']?['name'] |
| Description | item()?['fields']?['description'] |
Important tips:
- Add each mapping row one by one through the GUI
- Do NOT paste a large JSON object - Power Automate will reject it
- Use the
?operator for safe null handling - Adjust field paths based on your custom field structure
5.2 Create CSV Table
Add action: Create CSV table
| Property | Value |
|---|---|
| From | Output of Select action |
| Columns | Automatic (or Custom if needed) |
This action converts the JSON array into CSV format with headers.
Step 6: Save CSV to SharePoint
Add action: Create file
| Property | Value |
|---|---|
| Site Address | https://contoso.sharepoint.com/sites/YourSite |
| Folder Path | /Shared Documents/JiraReports |
| File Name | JiraReport-@{formatDateTime(utcNow(),'yyyyMMddHHmmss')}.csv |
| File Content | Output of Create CSV table |
Replace placeholders:
contoso→ your tenant nameYourSite→ your SharePoint site nameJiraReports→ your document library path
This creates timestamped files like:
JiraReport-20251121093015.csv
JiraReport-20251205093022.csv
Step 7: Send Email with Attachment
Add action: Send an email (V2)
| Property | Value |
|---|---|
| To | reports@contoso.com |
| Subject | [Biweekly Jira Report] Filter 12345 - @{formatDateTime(utcNow(),'yyyy-MM-dd')} |
| Body | See HTML body below |
Email body (HTML example):
<p>Hello Team,</p>
<p>Please find attached the latest Jira Service Management report for filter 12345.</p>
<p>This report includes:</p>
<ul>
<li>All open and closed issues</li>
<li>Custom field values</li>
<li>Assignee and reporter information</li>
</ul>
<p>The CSV file is also stored in SharePoint at:</p>
<p><strong>/Shared Documents/JiraReports</strong></p>
<br/>
<p>Regards,<br/>Automation System</p>
Attachments:
| Property | Value |
|---|---|
| Attachments Name | JiraReport-@{formatDateTime(utcNow(),'yyyyMMdd')}.csv |
| Attachments Content | Output of Create CSV table |
Troubleshooting Guide
Common Issues and Solutions
1. Self-Reference Error on varAllIssues
Error message:
The action 'Set_variable' cannot use the variable 'varAllIssues' in its inputs
Cause:
Using union(variables('varAllIssues'), variables('varAllIssues'))
Solution:
Always use Append to array variable inside Apply to each
2. Parse JSON Keeps Failing
Error message:
Invalid type. Expected Object but got Array.
Cause:
Schema does not match the actual API response structure
Solution:
- Use Postman or similar tool to get a real API response
- Copy the entire JSON response
- In Power Automate Parse JSON action, click Use sample payload to generate schema
- Paste your real response
- Review and adjust the generated schema
3. Select Action Shows "Enter valid JSON"
Error message:
Invalid expression: expected JSON object
Cause:
Pasting a whole JSON object into a single mapping field
Solution:
- Add each column mapping one by one through the GUI
- For each column, only enter one expression
- Example:
item()?['fields']?['summary'](NOT an entire object)
4. Flow Only Returns First 100 Issues
Problem:
Report always contains exactly 100 issues, even though the filter has 500+
Cause:
Do untilcondition not set correctlyvarIsLastnot being updated from API responsenextPageTokennot being passed in HTTP body
Solution checklist:
-
Do untilcondition checksvarIsLast = true -
varIsLastis updated withbody('Parse_JSON')?['isLast'] -
nextPageTokenis passed in HTTP body as@{variables('varNextPageToken')} - No hardcoded
trueorfalsevalues in variable updates
5. Authentication 401 or 403 Errors
Error message:
401 Unauthorized
403 Forbidden
Cause:
- Invalid API token
- Insufficient permissions
- Wrong username/password format
Solution:
- Verify API token is valid (regenerate if necessary)
- Confirm your Jira user has permission to:
- View the filter
- Access all issues in the filter
- View all custom fields used
- Ensure username is your email address (not display name)
- Ensure password is the API token (not your Jira password)
Best Practices
Performance Optimization
-
Use
maxResults = 100- This is the optimal batch size for Jira API
- Larger values may cause timeouts
- Smaller values increase API call overhead
-
Use saved filters instead of complex JQL
"jql": "filter = 12345"is more maintainable- Complex JQL can be tested in Jira UI before hardcoding
-
Set appropriate loop limits
- Count: 60 (handles up to 6000 issues)
- Timeout: PT1H (1 hour maximum runtime)
Security Considerations
API Token Management - Choose Your Security Level
Important: This tutorial uses Level 1 (Basic Protection) - storing the API token directly in Power Automate's HTTP action. This is a secure and practical approach for most use cases. Level 2 (Azure Key Vault) is optional and only needed for specific enterprise scenarios.
Level 1: Basic Protection (Used in This Tutorial) ✓
Store the API token directly in the HTTP action's Authentication field:
| Property | Value |
|---|---|
| Authentication | Basic |
| Username | your.email@company.com |
| Password | ATATT3xFfGF0... (API Token) |
Security features:
- Power Automate automatically encrypts the token
- Token is not visible in exported flow definitions
- Access controlled by Power Automate permissions
When to use:
- Personal projects or proof-of-concept
- Small teams (< 5 people)
- Non-regulated environments
Not recommended when:
- ❌ Multiple flows share the same token (rotation becomes difficult)
- ❌ Compliance requirements mandate centralized secret management
- ❌ Regular token rotation is required
Level 2: Azure Key Vault (Optional - Enterprise Requirements Only)
Note: This level is only needed if your organization requires centralized secret management. Most users can skip this section.
Store tokens in Azure Key Vault and retrieve them dynamically:
Step 1: Store token in Azure Key Vault
az keyvault secret set \
--vault-name "YourKeyVault" \
--name "JiraApiToken" \
--value "ATATT3xFfGF0..."
Step 2: Add "Get secret" action in Power Automate
Before the "Do until" loop, add:
| Action | Get secret (Azure Key Vault) |
|---|---|
| Vault name | YourKeyVault |
| Secret name | JiraApiToken |
| Output variable | body('Get_secret')?['value'] |
Step 3: Reference in HTTP action
| Property | Value |
|---|---|
| Authentication | Basic |
| Username | your.email@company.com |
| Password | @{body('Get_secret')?['value']} |
Benefits: