Practical recipes and patterns for common automation tasks. All examples can be pasted directly into the Console. Replace PROJ-123 with your own issue keys.
Bulk operations
Update all issues in a project
const result = Issues.search('project = PROJ AND status = Open')
const report = result.updateAll({ priority: 'Medium' })
return `Updated ${report.success} issues, ${report.failed} failed`
Add a comment to multiple issues
const keys = ['PROJ-1', 'PROJ-2', 'PROJ-3']
for (const key of keys) {
Issues.get(key).addComment('Reviewed and approved.')
log(`Commented on ${key}`)
}
return `Commented on ${keys.length} issues`
Transition issues by JQL
Move issues matching a query to a target status.
const result = Issues.search('project = PROJ AND status = "To Do" AND created < -30d')
const report = result.transitionAll('In Progress')
return `Transitioned ${report.success}/${report.success + report.failed}`
Bulk assign issues
const user = Users.current()
const result = Issues.search('project = PROJ AND assignee is EMPTY AND status = Open')
const report = result.forEach((issue) => {
issue.assign(user.accountId)
})
return `Assigned ${report.success} issues to ${user.displayName}`
Scenario batch operations (large scale)
For processing thousands of issues, use batch() in scenarios. Each page of results runs as a separate invocation with automatic pagination, progress tracking, and error handling.
step('count', () => {
return { total: Issues.count('project = PROJ') }
})
batch('cleanup', 'project = PROJ AND updated < -180d', (issue) => {
issue.transition('Done')
})
step('report', (ctx) => {
log(`Done: ${ctx.cleanup.processed}/${ctx.count.total}, ${ctx.cleanup.failed} errors`)
})
Batch with config object:
batch('export', {
jql: 'project = PROJ',
fields: ['summary', 'status', 'priority'],
batchSize: 50,
maxIssues: 10000,
continueOnError: true,
}, (issue) => {
log(issue.key + ': ' + issue.summary)
})
Reporting
Issue count by status
const result = Issues.search('project = PROJ')
const counts = result.countBy('status')
log('Issue counts by status:')
for (let [status, count] of Object.entries(counts)) {
log(` ${status}: ${count}`)
}
return counts
Overdue issues report
const result = Issues.search(
'project = PROJ AND duedate < now() AND status != Done'
)
const overdue = result.filter(issue => issue.isOverdue)
log(`Found ${overdue.length} overdue issues:`)
for (const issue of overdue) {
log(` ${issue.key}: ${issue.summary} (due: ${issue.dueDate})`)
}
return { count: overdue.length }
Assignee workload
const result = Issues.search('project = PROJ AND status != Done')
const workload = result.countBy('assignee')
const sorted = Object.entries(workload).sort((a, b) => b[1] - a[1])
log('Workload:')
for (const [name, count] of sorted) {
log(` ${name}: ${count} issues`)
}
return Object.fromEntries(sorted)
Created vs resolved trend
const created = Issues.search('project = PROJ AND created >= -7d', { includeTotal: true })
const resolved = Issues.search('project = PROJ AND resolved >= -7d', { includeTotal: true })
return {
period: 'Last 7 days',
created: created.total,
resolved: resolved.total,
net: created.total - resolved.total
}
Component coverage
const project = Projects.get('PROJ')
const components = project.getComponents()
let results = []
for (const comp of components) {
const issues = Issues.search(
`project = PROJ AND component = "${comp.name}"`,
{ includeTotal: true }
)
results.push({ name: comp.name, count: issues.total })
}
results.sort((a, b) => b.count - a.count)
return results
Field sync
Sync labels from parent to subtasks
const parent = Issues.get('PROJ-100')
const subtasks = Issues.search(`parent = ${parent.key}`)
const report = subtasks.forEach((sub) => {
sub.update({ labels: parent.labels })
log(`Synced labels to ${sub.key}`)
})
return `Synced labels to ${report.success} subtasks`
Copy priority from epic to stories
const epic = Issues.get('PROJ-50')
const stories = Issues.search(`"Epic Link" = ${epic.key}`)
let updated = 0
for (const story of stories.issues) {
if (story.priority !== epic.priority) {
story.update({ priority: epic.priority })
updated++
}
}
return `Updated priority on ${updated}/${stories.issues.length} stories`
Sync custom field across project
const source = Issues.get('PROJ-100')
const value = source.field('Story Points')
const targets = Issues.search(
'project = PROJ AND "Epic Link" = PROJ-100 AND "Story Points" is EMPTY'
)
const report = targets.forEach((target) => {
target.update({ 'Story Points': value })
})
return `Synced Story Points to ${report.success} issues`
Table operations
Basic CRUD
// Add a row
tables.addRow('inventory', {
name: 'Widget A',
quantity: 100,
category: 'hardware'
})
// Find and update
let row = tables.findRow('inventory', { name: 'Widget A' })
if (row) {
tables.updateRow('inventory', row.id, {
quantity: row.data.quantity - 10
})
}
// Delete by condition
let deleted = tables.deleteRows('inventory', { quantity: 0 })
return `Deleted ${deleted} empty rows`
Upsert pattern
const upserted = tables.upsert('inventory',
{ name: 'Widget A' },
{ name: 'Widget A', quantity: 100, category: 'hardware' }
)
log(upserted._action) // "created" or "updated"
Sync Jira data to a table
const issues = Issues.search(
'project = PROJ AND status = Done AND resolved >= -7d',
{ fields: ['summary', 'assignee', 'resolved'] }
)
let data = issues.map(i => ({
issueKey: i.key,
summary: i.summary,
assignee: i.assignee || 'None',
resolved: i.created
}))
tables.addRows('resolved-report', data)
return `Exported ${data.length} issues to table`
Count and statistics
let total = tables.count('inventory')
let lowStock = tables.count('inventory', { quantity: { $lt: 10 } })
let outOfStock = tables.count('inventory', { quantity: 0 })
return { total, lowStock, outOfStock, healthy: total - lowStock }
Queue patterns
Basic producer/consumer
Producer (pushes messages):
let issues = Issues.search('project = PROJ AND status = "To Do" AND created >= -1d')
for (let issue of issues.issues) {
queue.push('pending-review', {
issueKey: issue.key,
summary: issue.summary,
})
}
return `Queued ${issues.issues.length} issues for review`
Consumer (processes messages):
let messages = queue.pull('pending-review', 5)
for (let msg of messages) {
try {
Issues.get(msg.payload.issueKey).addComment('Auto-reviewed by script')
queue.ack(msg.id)
} catch (e) {
queue.reject(msg.id)
}
}
return `Processed ${messages.length} messages`
Priority queue
// Push with different priorities (higher = processed first)
queue.push('notifications', { type: 'critical', text: 'Server down' }, 10)
queue.push('notifications', { type: 'warning', text: 'High memory' }, 5)
queue.push('notifications', { type: 'info', text: 'Deploy complete' }, 1)
// Pull returns highest priority first
let msgs = queue.pull('notifications', 3)
for (let msg of msgs) {
log(`[${msg.payload.type}] ${msg.payload.text}`)
}
Consume pattern (one-shot processing)
let messages = queue.consume('email-queue', 5)
for (let msg of messages) {
log(`Sending email to ${msg.payload.to}: ${msg.payload.subject}`)
// Messages are already removed from the queue
}
return `Consumed ${messages.length} messages`
Queue size monitoring
let queues = ['work-queue', 'email-queue', 'notification-queue']
let report = []
for (let name of queues) {
let stats = queue.stats(name)
report.push({ name, ...stats })
if (stats.failed > 0) {
warn(`Queue "${name}" has ${stats.failed} failed messages`)
}
}
return report
Automation recipes
Scheduled: daily stale issue reminder
const result = Issues.search(
'project = PROJ AND status != Done AND updated < -14d'
)
const report = result.forEach((issue) => {
issue.addComment('This issue has not been updated in 14 days. Please review.')
})
return `Reminded ${report.success} stale issues`
Scheduled: weekly status report to table
let open = Issues.search('project = PROJ AND status != Done', { includeTotal: true })
let created = Issues.search('project = PROJ AND created >= -7d', { includeTotal: true })
let resolved = Issues.search('project = PROJ AND resolved >= -7d', { includeTotal: true })
tables.addRow('weekly-stats', {
date: Date.create().jiraDate(),
open: open.total,
created: created.total,
resolved: resolved.total
})
return { open: open.total, created: created.total, resolved: resolved.total }
Event: auto-assign based on component
// Triggers on issue_created
let assignments = {
'Frontend': '5a1234567890abcdef',
'Backend': '5b1234567890abcdef',
'DevOps': '5c1234567890abcdef'
}
let components = issue.components || []
if (components.length > 0) {
let accountId = assignments[components[0]]
if (accountId) {
issue.assign(accountId)
log(`Assigned ${issue.key} to ${components[0]} owner`)
}
}
UIM: Make Field Required Based on Priority
if (uimData.callbackType === 'onInit' || uimData.changedFieldId === 'priority') {
let priority = uimData.fieldValues?.priority?.value
if (priority === 'Highest' || priority === 'High') {
uim.setRequired('duedate', true)
uim.setDescription('duedate', 'Due date is required for high priority issues')
} else {
uim.setRequired('duedate', false)
uim.setDescription('duedate', '')
}
}
UIM: Show/Hide Fields Dynamically
if (uimData.callbackType === 'onInit' || uimData.changedFieldId === 'issuetype') {
let type = uimData.fieldValues?.issuetype?.value
if (type === 'Bug') {
uim.setVisible('environment', true)
uim.setVisible('customfield_10100', true) // Steps to reproduce
} else {
uim.setVisible('environment', false)
uim.setVisible('customfield_10100', false)
}
}
Scripted field: calculate days since created
let created = Date.parse(issue.created)
let days = Math.floor((Date.now() - created) / (1000 * 60 * 60 * 24))
return `${days} days`
Scripted field: sum subtask story points
const subtasks = Issues.search(`parent = ${issue.key}`)
let total = 0
for (const sub of subtasks.issues) {
let sp = sub.field('Story Points')
total += sp || 0
}
return total
Next steps
- Scripting Language - Language reference
- Scripting API: Issues - Issue API
- Scripting API: Tables - Tables API
- Scripting API: Queues - Queue API
- Scenarios - Multi-step processing
- Triggers - Trigger configuration
