Move Jira issues through workflow states. Use when transitioning issues (To Do, In Progress, Done) or setting resolutions.
Move issues through workflow states. Get available transitions and execute status changes.
Transition IDs are NOT standardized - they vary by:
Always query available transitions first before attempting to transition.
interface Transition {
id: string;
name: string;
to: {
id: string;
name: string;
statusCategory: {
id: number;
key: string;
name: string;
};
};
fields?: Record<string, {
required: boolean;
name: string;
allowedValues?: Array<{ id: string; name: string }>;
}>;
}
interface TransitionsResponse {
transitions: Transition[];
}
async function getTransitions(
client: JiraClient,
issueKeyOrId: string
): Promise<Transition[]> {
const response = await client.request<TransitionsResponse>(
`/issue/${issueKeyOrId}/transitions?expand=transitions.fields`
);
return response.transitions;
}
async function findTransitionByName(
client: JiraClient,
issueKeyOrId: string,
targetStatusName: string
): Promise<Transition | null> {
const transitions = await getTransitions(client, issueKeyOrId);
return transitions.find(
t => t.name.toLowerCase() === targetStatusName.toLowerCase() ||
t.to.name.toLowerCase() === targetStatusName.toLowerCase()
) || null;
}
interface TransitionOptions {
resolution?: { name: string } | { id: string };
comment?: string;
fields?: Record<string, any>;
}
async function transitionIssue(
client: JiraClient,
issueKeyOrId: string,
transitionId: string,
options: TransitionOptions = {}
): Promise<void> {
const body: any = {
transition: { id: transitionId },
};
if (options.resolution || options.fields) {
body.fields = { ...options.fields };
if (options.resolution) {
body.fields.resolution = options.resolution;
}
}
if (options.comment) {
body.update = {
comment: [
{
add: {
body: {
type: 'doc',
version: 1,
content: [
{
type: 'paragraph',
content: [{ type: 'text', text: options.comment }],
},
],
},
},
},
],
};
}
await client.request(`/issue/${issueKeyOrId}/transitions`, {
method: 'POST',
body: JSON.stringify(body),
});
}
async function moveIssueTo(
client: JiraClient,
issueKeyOrId: string,
targetStatus: string,
options: TransitionOptions = {}
): Promise<boolean> {
const transition = await findTransitionByName(client, issueKeyOrId, targetStatus);
if (!transition) {
console.error(`No transition found to status: ${targetStatus}`);
return false;
}
// Check if resolution is required
if (transition.fields?.resolution?.required && !options.resolution) {
// Default to "Done" resolution
options.resolution = { name: 'Done' };
}
await transitionIssue(client, issueKeyOrId, transition.id, options);
return true;
}
async function bulkTransition(
client: JiraClient,
issueKeys: string[],
targetStatus: string,
options: TransitionOptions = {}
): Promise<{ success: string[]; failed: string[] }> {
const results = { success: [] as string[], failed: [] as string[] };
for (const issueKey of issueKeys) {
try {
const success = await moveIssueTo(client, issueKey, targetStatus, options);
if (success) {
results.success.push(issueKey);
} else {
results.failed.push(issueKey);
}
} catch (error) {
results.failed.push(issueKey);
}
}
return results;
}
Most Jira projects have these standard transitions:
| From Status | Transition Name | To Status |
|---|---|---|
| To Do | Start Progress | In Progress |
| In Progress | Done | Done |
| In Progress | Stop Progress | To Do |
| Done | Reopen | To Do |
Note: These names vary by workflow configuration.
When transitioning to "Done", you often need a resolution:
| Resolution | Description |
|---|---|
| Done | Work completed |
| Won't Do | Not planning to do |
| Duplicate | Already exists |
| Cannot Reproduce | Cannot reproduce issue |
curl -X GET "$JIRA_BASE_URL/rest/api/3/issue/SCRUM-123/transitions?expand=transitions.fields" \
-H "Authorization: Basic $(echo -n 'email:token' | base64)" \
-H "Accept: application/json"
curl -X POST "$JIRA_BASE_URL/rest/api/3/issue/SCRUM-123/transitions" \
-H "Authorization: Basic $(echo -n 'email:token' | base64)" \
-H "Content-Type: application/json" \
-d '{
"transition": { "id": "21" }
}'
curl -X POST "$JIRA_BASE_URL/rest/api/3/issue/SCRUM-123/transitions" \
-H "Authorization: Basic $(echo -n 'email:token' | base64)" \
-H "Content-Type: application/json" \
-d '{
"transition": { "id": "31" },
"fields": {
"resolution": { "name": "Done" }
}
}'
curl -X POST "$JIRA_BASE_URL/rest/api/3/issue/SCRUM-123/transitions" \
-H "Authorization: Basic $(echo -n 'email:token' | base64)" \
-H "Content-Type: application/json" \
-d '{
"transition": { "id": "21" },
"update": {
"comment": [
{
"add": {
"body": {
"type": "doc",
"version": 1,
"content": [
{
"type": "paragraph",
"content": [{ "type": "text", "text": "Moving to In Progress" }]
}
]
}
}
}
]
}
}'
A successful transition returns 204 No Content with an empty body.
| Error | Cause | Solution |
|---|---|---|
| 400 Bad Request | Invalid transition ID | Query transitions first |
| 400 Bad Request | Missing required resolution | Add resolution field |
| 403 Forbidden | No permission to transition | Check workflow permissions |
| 404 Not Found | Issue doesn't exist | Verify issue key |
{
"errorMessages": [
"You must specify a resolution when transitioning issues to the 'Done' status."
],
"errors": {
"resolution": "Resolution is required."
}
}
async function discoverWorkflow(
client: JiraClient,
issueKeyOrId: string
): Promise<Map<string, Transition[]>> {
// Get transitions from current state
const transitions = await getTransitions(client, issueKeyOrId);
console.log(`Available transitions from current state:`);
for (const t of transitions) {
console.log(` ${t.id}: ${t.name} → ${t.to.name}`);
if (t.fields?.resolution?.required) {
console.log(` (requires resolution)`);
}
}
return new Map([
['current', transitions]
]);
}