jira-safe
Implement SAFe methodology in Jira. Use when creating Epics, Features, Stories with proper hierarchy, acceptance criteria, and parent-child linking.
Best use case
jira-safe is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Implement SAFe methodology in Jira. Use when creating Epics, Features, Stories with proper hierarchy, acceptance criteria, and parent-child linking.
Teams using jira-safe should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/jira-safe/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How jira-safe Compares
| Feature / Agent | jira-safe | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Implement SAFe methodology in Jira. Use when creating Epics, Features, Stories with proper hierarchy, acceptance criteria, and parent-child linking.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# Jira SAFe (Scaled Agile Framework) Skill
> Implements SAFe methodology for Epic, Feature, Story, and Task management in Jira Cloud.
## When to Use
- Creating Epics with business outcomes and acceptance criteria
- Writing user stories in SAFe format ("As a... I want... So that...")
- Breaking down Features into Stories with acceptance criteria
- Creating Subtasks under Stories
- Linking work items in proper hierarchy (Epic → Feature → Story → Subtask)
## CRITICAL: Next-Gen vs Classic Projects
**SCRUM project is Next-Gen (Team-managed)**. Key differences:
| Aspect | Classic (Company-managed) | Next-Gen (Team-managed) |
|--------|---------------------------|-------------------------|
| Epic Link | `customfield_10014` | `parent: { key: 'EPIC-KEY' }` |
| Epic Name | `customfield_10011` | Not available |
| Subtask Type | `'Sub-task'` | `'Subtask'` |
| Project Style | `classic` | `next-gen`, `simplified: true` |
**Always detect project type first:**
```javascript
const projectInfo = await fetch(`${JIRA_URL}/rest/api/3/project/${PROJECT_KEY}`, { headers });
const project = await projectInfo.json();
const isNextGen = project.style === 'next-gen' || project.simplified === true;
```
## SAFe Hierarchy in Jira
```
Portfolio Level:
└── Epic (Strategic Initiative)
└── Feature (Benefit Hypothesis)
└── Story (User Value)
└── Subtask (Technical Work)
```
## SAFe Templates
### Epic Template (Next-Gen)
```javascript
// NOTE: Next-Gen projects do NOT use customfield_10011 (Epic Name)
const epic = {
fields: {
project: { key: 'PROJECT_KEY' },
issuetype: { name: 'Epic' },
summary: '[Epic ID]: [Epic Name] - [Business Outcome]',
description: {
type: 'doc',
version: 1,
content: [
{
type: 'heading',
attrs: { level: 2 },
content: [{ type: 'text', text: 'Business Outcome' }]
},
{
type: 'paragraph',
content: [{ type: 'text', text: 'Describe the measurable business value...' }]
},
{
type: 'heading',
attrs: { level: 2 },
content: [{ type: 'text', text: 'Success Metrics' }]
},
{
type: 'bulletList',
content: [
{
type: 'listItem',
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Metric 1: [measurable target]' }] }]
}
]
},
{
type: 'heading',
attrs: { level: 2 },
content: [{ type: 'text', text: 'Scope' }]
},
{
type: 'paragraph',
content: [{ type: 'text', text: 'What is in scope and out of scope...' }]
}
]
},
labels: ['epic-label'] // Use labels instead of Epic Name for categorization
}
};
```
### Story Template (SAFe Format, Next-Gen)
```javascript
// NOTE: Next-Gen uses 'parent' field, NOT customfield_10014
const story = {
fields: {
project: { key: 'PROJECT_KEY' },
issuetype: { name: 'Story' },
summary: '[US-ID]: As a [persona], I want [goal], so that [benefit]',
description: {
type: 'doc',
version: 1,
content: [
{
type: 'heading',
attrs: { level: 2 },
content: [{ type: 'text', text: 'User Story' }]
},
{
type: 'paragraph',
content: [
{ type: 'text', text: 'As a ', marks: [{ type: 'strong' }] },
{ type: 'text', text: '[persona]' },
{ type: 'text', text: ', I want ', marks: [{ type: 'strong' }] },
{ type: 'text', text: '[goal]' },
{ type: 'text', text: ', so that ', marks: [{ type: 'strong' }] },
{ type: 'text', text: '[benefit]' }
]
},
{
type: 'heading',
attrs: { level: 2 },
content: [{ type: 'text', text: 'Acceptance Criteria' }]
},
{
type: 'heading',
attrs: { level: 3 },
content: [{ type: 'text', text: 'Scenario 1: [Name]' }]
},
{
type: 'bulletList',
content: [
{
type: 'listItem',
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'GIVEN [precondition]', marks: [{ type: 'strong' }] }] }]
},
{
type: 'listItem',
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'WHEN [action]', marks: [{ type: 'strong' }] }] }]
},
{
type: 'listItem',
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'THEN [expected result]', marks: [{ type: 'strong' }] }] }]
}
]
},
{
type: 'heading',
attrs: { level: 2 },
content: [{ type: 'text', text: 'Definition of Done' }]
},
{
type: 'bulletList',
content: [
{
type: 'listItem',
content: [{ type: 'paragraph', content: [{ type: 'text', text: '[ ] Code reviewed and approved' }] }]
},
{
type: 'listItem',
content: [{ type: 'paragraph', content: [{ type: 'text', text: '[ ] Unit tests written and passing' }] }]
},
{
type: 'listItem',
content: [{ type: 'paragraph', content: [{ type: 'text', text: '[ ] Integration tests passing' }] }]
},
{
type: 'listItem',
content: [{ type: 'paragraph', content: [{ type: 'text', text: '[ ] Documentation updated' }] }]
}
]
}
]
},
// Next-Gen: Link to parent Epic using 'parent' field
parent: { key: 'EPIC_KEY' },
labels: ['category-label', 'epic-id']
}
};
```
### Subtask Template (Next-Gen)
```javascript
// NOTE: Next-Gen uses 'Subtask' (no hyphen), NOT 'Sub-task'
const subtask = {
fields: {
project: { key: 'PROJECT_KEY' },
issuetype: { name: 'Subtask' }, // Next-Gen: 'Subtask', Classic: 'Sub-task'
summary: '[Technical task description]',
// Parent Story (required for subtasks)
parent: { key: 'STORY_KEY' }
// Note: Description is optional for subtasks
}
};
```
## API Implementation (Next-Gen Projects)
### Create Epic with Stories (Next-Gen)
```javascript
async function createEpicWithStories(epicFields, storyDefinitions) {
const headers = {
'Authorization': `Basic ${Buffer.from(`${EMAIL}:${TOKEN}`).toString('base64')}`,
'Content-Type': 'application/json',
'Accept': 'application/json'
};
// 1. Create Epic
const epicResponse = await fetch(`${JIRA_URL}/rest/api/3/issue`, {
method: 'POST',
headers,
body: JSON.stringify({ fields: epicFields })
});
if (!epicResponse.ok) {
const error = await epicResponse.text();
throw new Error(`Epic creation failed: ${error}`);
}
const createdEpic = await epicResponse.json();
console.log(`Created Epic: ${createdEpic.key}`);
// 2. Create Stories linked to Epic using 'parent' field (Next-Gen)
const createdStories = [];
for (const storyDef of storyDefinitions) {
const storyFields = {
...storyDef,
parent: { key: createdEpic.key } // Next-Gen: use 'parent', NOT customfield_10014
};
const storyResponse = await fetch(`${JIRA_URL}/rest/api/3/issue`, {
method: 'POST',
headers,
body: JSON.stringify({ fields: storyFields })
});
if (!storyResponse.ok) {
const error = await storyResponse.text();
console.error(`Story creation failed: ${error}`);
continue;
}
const createdStory = await storyResponse.json();
createdStories.push(createdStory);
console.log(` Created Story: ${createdStory.key}`);
// Rate limiting
await new Promise(r => setTimeout(r, 100));
}
return { epic: createdEpic, stories: createdStories };
}
```
### Create Story with Subtasks (Next-Gen)
```javascript
async function createStoryWithSubtasks(storyFields, epicKey, subtaskSummaries) {
const headers = {
'Authorization': `Basic ${Buffer.from(`${EMAIL}:${TOKEN}`).toString('base64')}`,
'Content-Type': 'application/json',
'Accept': 'application/json'
};
// 1. Create Story under Epic
const storyRequest = {
fields: {
...storyFields,
parent: { key: epicKey } // Link to Epic
}
};
const storyResponse = await fetch(`${JIRA_URL}/rest/api/3/issue`, {
method: 'POST',
headers,
body: JSON.stringify(storyRequest)
});
if (!storyResponse.ok) {
throw new Error(`Story creation failed: ${await storyResponse.text()}`);
}
const createdStory = await storyResponse.json();
// 2. Create Subtasks under Story
const createdSubtasks = [];
for (const summary of subtaskSummaries) {
const subtaskResponse = await fetch(`${JIRA_URL}/rest/api/3/issue`, {
method: 'POST',
headers,
body: JSON.stringify({
fields: {
project: { key: storyFields.project.key },
issuetype: { name: 'Subtask' }, // Next-Gen: 'Subtask', NOT 'Sub-task'
summary: summary,
parent: { key: createdStory.key }
}
})
});
if (subtaskResponse.ok) {
createdSubtasks.push(await subtaskResponse.json());
}
await new Promise(r => setTimeout(r, 50)); // Rate limiting
}
return { story: createdStory, subtasks: createdSubtasks };
}
```
### Get Epic Link Field ID
Epic link field varies by Jira instance. Find it:
```javascript
async function findEpicLinkField() {
const response = await fetch(`${JIRA_URL}/rest/api/3/field`, { headers });
const fields = await response.json();
const epicLinkField = fields.find(f =>
f.name === 'Epic Link' ||
f.name.toLowerCase().includes('epic link')
);
return epicLinkField?.id; // Usually customfield_10014
}
```
### Bulk Delete Issues
```javascript
async function bulkDeleteIssues(projectKey, maxResults = 100) {
// Search for all issues
const jql = encodeURIComponent(`project = ${projectKey} ORDER BY key ASC`);
const searchResponse = await fetch(
`${JIRA_URL}/rest/api/3/search/jql?jql=${jql}&maxResults=${maxResults}&fields=key`,
{ headers }
);
const { issues } = await searchResponse.json();
// Delete each issue
for (const issue of issues) {
await fetch(`${JIRA_URL}/rest/api/3/issue/${issue.key}?deleteSubtasks=true`, {
method: 'DELETE',
headers
});
console.log(`Deleted: ${issue.key}`);
await new Promise(r => setTimeout(r, 100)); // Rate limit
}
return issues.length;
}
```
## SAFe Best Practices
### Epic Naming
- Format: `[Domain] - [Business Outcome]`
- Example: `Marketing Copilot - Enable 24/7 Brand-Aware Content Generation`
### Story Naming (INVEST Criteria)
- **I**ndependent: Can be developed separately
- **N**egotiable: Details can be discussed
- **V**aluable: Delivers user value
- **E**stimable: Can be sized
- **S**mall: Fits in a sprint
- **T**estable: Has clear acceptance criteria
### Story Format
```
As a [specific persona],
I want [concrete action/capability],
So that [measurable benefit].
```
### Acceptance Criteria (Given-When-Then)
```
Scenario: [Descriptive name]
GIVEN [initial context/precondition]
WHEN [action/event occurs]
THEN [expected outcome]
AND [additional outcome if needed]
```
## Issue Link Types (Next-Gen)
| Link Type | Use Case | Field |
|-----------|----------|-------|
| Parent (Next-Gen) | Story → Epic | `parent: { key: 'EPIC-KEY' }` |
| Parent (Next-Gen) | Subtask → Story | `parent: { key: 'STORY-KEY' }` |
| Blocks/Is blocked by | Dependencies | Link type |
| Relates to | Related items | Link type |
**Classic Projects Only:**
| Link Type | Use Case | Field |
|-----------|----------|-------|
| Epic Link | Story → Epic | `customfield_10014` |
| Epic Name | Epic short name | `customfield_10011` |
## Custom Fields by Project Type
### Next-Gen (Team-managed) - SCRUM Project
| Purpose | Method |
|---------|--------|
| Link Story to Epic | `parent: { key: 'EPIC-KEY' }` |
| Link Subtask to Story | `parent: { key: 'STORY-KEY' }` |
| Subtask issue type | `issuetype: { name: 'Subtask' }` |
### Classic (Company-managed)
| Field | ID (typical) | Purpose |
|-------|--------------|---------|
| Epic Link | customfield_10014 | Links Story to Epic |
| Epic Name | customfield_10011 | Short name for Epic |
| Story Points | customfield_10016 | Estimation |
| Sprint | customfield_10007 | Sprint assignment |
## Error Handling
```javascript
async function safeJiraRequest(url, options = {}) {
const response = await fetch(url, { ...options, headers });
if (!response.ok) {
const error = await response.text();
throw new Error(`Jira API ${response.status}: ${error.substring(0, 200)}`);
}
if (response.status === 204) return null;
return response.json();
}
```
## References
- [SAFe Framework](https://scaledagileframework.com/)
- [Jira REST API v3](https://developer.atlassian.com/cloud/jira/platform/rest/v3/)
- [Atlassian Document Format](https://developer.atlassian.com/cloud/jira/platform/apis/document/structure/)Related Skills
jira-workflow-creator
Jira Workflow Creator - Auto-activating skill for Enterprise Workflows. Triggers on: jira workflow creator, jira workflow creator Part of the Enterprise Workflows skill category.
jira-ticket-generator
Jira Ticket Generator - Auto-activating skill for Enterprise Workflows. Triggers on: jira ticket generator, jira ticket generator Part of the Enterprise Workflows skill category.
repomix-safe-mixer
Safely package codebases with repomix by automatically detecting and removing hardcoded credentials before packing. Use when packaging code for distribution, creating reference packages, or when the user mentions security concerns about sharing code with repomix.
safe-file-reader
Read files from documents directory safely
safe-calculator
A safe calculator for mathematical expressions
jira-expert
Atlassian Jira expert for creating and managing projects, planning, product discovery, JQL queries, workflows, custom fields, automation, reporting, and all Jira features. Use for Jira project setup, configuration, advanced search, dashboard creation, workflow design, and technical Jira operations.
jira-automation
Automate Jira tasks via Rube MCP (Composio): issues, projects, sprints, boards, comments, users. Always search tools first for current schemas.
azure-ai-contentsafety-ts
Analyze text and images for harmful content using Azure AI Content Safety (@azure-rest/ai-content-safety). Use when moderating user-generated content, detecting hate speech, violence, sexual content, or self-harm, or managing custom blocklists.
azure-ai-contentsafety-py
Azure AI Content Safety SDK for Python. Use for detecting harmful content in text and images with multi-severity classification. Triggers: "azure-ai-contentsafety", "ContentSafetyClient", "content moderation", "harmful content", "text analysis", "image analysis".
azure-ai-contentsafety-java
Build content moderation applications with Azure AI Content Safety SDK for Java. Use when implementing text/image analysis, blocklist management, or harm detection for hate, violence, sexual content, and self-harm.
safe-edit
Automatically backs up files, saves diffs, uses agents/skills, and ensures modular code (<200 lines) before any implementation. Use this skill for ALL code changes to ensure safe, reversible, and clean implementations.
jira-cli
Interact with Jira from the command line to create, list, view, edit, and transition issues, manage sprints and epics, and perform common Jira workflows. Use when the user asks about Jira tasks, tickets, issues, sprints, or needs to manage project work items.