Developer Portal for YouTrack and Hub Help

Helpdesk

This workflow is used to automate common use cases that are relevant to a helpdesk project.

Name

@jetbrains/youtrack-workflow-helpdesk

Auto-attached

no

Rules

  • Merge duplicate tickets (on-change rule)

  • Merge tickets manually (action rule)

  • Reopen ticket when reporter adds a comment (on-change rule)

  • New ticket from comment (action rule)

  • Remind reporters and close pending tickets automatically (on-schedule rule)

  • Assign ticket to author of first reply (on-change rule)

Use Case

This workflow provides extended functionality that is relevant to helpdesk projects.

When a helpdesk agent encounters a ticket that matches a support request that was previously submitted by the same reporter, they can mark one of the tickets as a duplicate. This helps to consolidate communication in a single thread and prevent agents from providing conflicting answers. Agents can either mark the ticket as a duplicate using the Duplicates link or by using the Merge ticket manually option in the action menu for the ticket.

When an agent receives a reply from a reporter that should be handled as a separate support request, they can use the New ticket from comment action. This takes the comment text and copies it to the description field in the new ticket and prompts the agent to enter provide the summary.

When an agent has replied to a reporter in a ticket, they can move it to a Pending state to pause the SLA timer. This state indicates that the agent is waiting for the reporter to answer a question or confirm the action taken.

  • If the reporter doesn't reply within a specified timeframe, a reminder is sent to the reporter.

  • If the agent doesn't hear back from the reporter after a certain amount of time, the ticket is closed automatically. An additional message is sent to the reporter to inform them that the ticket was closed.

When a reporter adds a comment to a ticket that is set as Pending, it is reopened automatically. This helps to ensure that the resolution time is tracked according to the active SLA policy.

Modules

This workflow supports several rules that you can use to handle tickets in a helpdesk project.

Merge duplicate tickets

This rule is used to manage duplicate ticket in a helpdesk project. When a ticket is marked as a duplicate of another ticket, this on-change rule checks whether both tickets were reported by the same user.

  • If the tickets were reported by different people, an error message is shown to the agent and the operation is reverted.

  • When the same person reported both tickets, a public comment is posted in the ticket that is marked as a duplicate. The comment includes a link to the currently active ticket.

const entities = require('@jetbrains/youtrack-scripting-api/entities'); const workflow = require('@jetbrains/youtrack-scripting-api/workflow'); exports.rule = entities.Issue.onChange({ title: 'Merge duplicate tickets', guard: (ctx) => { return ctx.issue.links['duplicates'].added.isNotEmpty(); }, action: (ctx) => { const duplicate = ctx.issue.links['duplicates'].added.first(); workflow.check(duplicate.reporter.login === ctx.issue.reporter.login, workflow.i18n("You can only merge tickets where the reporter is the same person")) const commnet = ctx.issue.addComment(workflow.i18n("This ticket was merged into {0}, which was also reported by you.", duplicate.id)) commnet.permittedUsers.clear() commnet.permittedGroups.clear() }, requirements: { Duplicate: { type: entities.IssueLinkPrototype, outward: 'is duplicated by', inward: 'duplicates' } } });

Merge tickets manually

This action rule lets an agent merge duplicate tickets using the merge with command. When an agent specifies this command, they are prompted to provide the ID of the ticket that want to merge with.

This action then applies the Duplicates link to the current ticket, which triggers the Merge duplicate tickets workflow rule described above.

const entities = require('@jetbrains/youtrack-scripting-api/entities'); exports.rule = entities.Issue.action({ title: 'Merge tickets manually', command: 'merge with', guard: function (ctx) { return ctx.issue.project.isAgent(ctx.currentUser) && ctx.issue.isReported; }, action: function (ctx) { const source = ctx.issue; const target = ctx.userInput; source.links['duplicates'].add(target); }, requirements: { Duplicate: { type: entities.IssueLinkPrototype, outward: 'is duplicated by', inward: 'duplicates' } }, userInput: { type: entities.Issue, description: 'Target ticket for merge' } });

Reopen when reporter adds a comment

This on-change rule automatically changes the value for the State field from Pending to Open whenever a comment is added by the reporter.

const entities = require('@jetbrains/youtrack-scripting-api/entities'); exports.rule = entities.Issue.onChange({ title: 'Reopen ticket when reporter adds a comment', guard: (ctx) => { return !ctx.issue.comments.added.isEmpty() && ctx.issue.reporter.login === ctx.issue.comments.added.first().author.login && ctx.issue.fields.State.name === ctx.State.Pending.name; }, action: (ctx) => { ctx.issue.fields.State = ctx.State.Open; }, requirements: { State: { type: entities.State.fieldType, Pending: {}, Open: {} } } });

New ticket from comment

This action rule lets agents use the text from a comment in a ticket as the basis for reporting another ticket in the helpdesk project. The agent is prompted to provide a summary for the new ticket.

When this action is applied, a comment that references the new ticket is posted in the original ticket. A comment that contains a link to the original comment is also added to the new ticket.

const entities = require('@jetbrains/youtrack-scripting-api/entities'); const workflow = require('@jetbrains/youtrack-scripting-api/workflow'); exports.rule = entities.IssueComment.action({ title: 'New ticket from comment', command: 'toTicket', guard: function (ctx) { return ctx.issueComment.permittedGroups.isEmpty() && ctx.issueComment.permittedUsers.isEmpty() && ctx.issueComment.issue.project.isAgent(ctx.currentUser) }, action: function (ctx) { const description = ctx.issueComment.text; let issue = ctx.issueComment.issue; const author = issue.reporter; const project = issue.project; const commentIssue = new entities.Issue(author, project, ctx.userInput); commentIssue.description = description; const info = issue.addComment(workflow.i18n('A related ticket ({0}) has been created based on a comment from this ticket.', commentIssue.id)) commentIssue.addComment(workflow.i18n('This ticket was created from a [comment in another ticket]({0}).', ctx.issueComment.url)) info.permittedUsers.clear() info.permittedGroups.clear() }, userInput: { type: 'string', description: 'Ticket summary' } });

Remind reporters and close pending tickets automatically

This on-schedule rule checks for tickets where the value for the State field are Pending.

  • If the ticket has been pending for more than 7 days, a reminder is sent to the reporter.

  • A second reminder is sent after 14 days.

  • If the ticket is still pending after 21 days, a comment is posted to the ticket and its state is set as Solved.

var entities = require('@jetbrains/youtrack-scripting-api/entities'); var workflow = require('@jetbrains/youtrack-scripting-api/workflow'); const firstReminderPolicyDays = 7; const secondReminderPolicyDays = 14; const retentionPolicyDays = 21; const oneDayMillis = 24 * 60 * 60 * 1000; exports.rule = entities.Issue.onSchedule({ title: "Remind reporters and close pending tickets automatically", cron: '0 0 0 * * ?', search: 'State: Pending', action: function (ctx) { const reporter = ctx.issue.reporter; let lastPublicComment = null; ctx.issue.comments.forEach(comment => { if (comment.author.login === reporter.login) { lastPublicComment = null; } else if (comment.permittedUsers.isEmpty() && comment.permittedGroups.isEmpty()) { lastPublicComment = comment; } }); if (lastPublicComment === null) { console.trace('Checking ticket ' + ctx.issue.id + ', last comment is not from the agent'); return; } const pendingIntervalDays = (ctx.ruleStarted - lastPublicComment.created) / oneDayMillis; if (pendingIntervalDays < firstReminderPolicyDays) { console.trace('Checking ticket ' + ctx.issue.id + ', too early to remind, pending = ' + pendingIntervalDays); return; } if (pendingIntervalDays > retentionPolicyDays + 1) { console.trace('Checking ticket ' + ctx.issue.id + ', too late to remind, pending = ' + pendingIntervalDays); return; } const firstReminderOverdue = pendingIntervalDays - firstReminderPolicyDays; if (firstReminderOverdue > 0 && firstReminderOverdue < 1) { reporter.notifyOnCase('reporterReminder', {}, ctx.issue); return; } const secondReminderOverdue = pendingIntervalDays - secondReminderPolicyDays; if (secondReminderOverdue > 0 && secondReminderOverdue < 1) { const params = { 'autoCloseDays': retentionPolicyDays - secondReminderPolicyDays }; reporter.notifyOnCase('reporterReminder', params, ctx.issue); return; } const autoCloseOverdue = pendingIntervalDays - retentionPolicyDays; if (autoCloseOverdue >= 0 && autoCloseOverdue < 1) { ctx.issue.fields.State = ctx.State.Solved; const params = { 'responseTimeoutDays': retentionPolicyDays }; reporter.notifyOnCase('reporterTicketClosed', params, ctx.issue); } }, requirements: { State: { type: entities.State.fieldType, Pending: {}, Solved: { name: workflow.i18n('Solved'), isResolved: true } } } });

If you want to customize the timeframe for sending reminders and closing abandoned tickets, you can modify the following values directly in the workflow code:

Value

Description

firstReminderPolicyDays

The number of days to wait before sending the first reminder.

secondReminderPolicyDays

The number of days to wait before sending the second reminder.

retentionPolicyDays

The number of days to wait before closing tickets automatically.

Assign ticket to author of first reply

When an unassigned ticket receives a comment from an agent, this on-change rule automatically assigns the ticket to the agent who is the comment's author.

const entities = require('@jetbrains/youtrack-scripting-api/entities'); exports.rule = entities.Issue.onChange({ title: 'Assign ticket to author of first reply', guard: (ctx) => { const issue = ctx.issue; if (!issue.fields.Assignee && issue.comments.added.isNotEmpty() && issue.project.isAgent(ctx.currentUser)) { let hasAgentComment = false; let result = true; issue.comments.forEach(function (comment) { if (result && issue.project.isAgent(comment.author)) { if (hasAgentComment) { result = false; } else { hasAgentComment = true; } } }) return result; } return false; }, action: (ctx) => { const issue = ctx.issue if (issue.project.findFieldByName(ctx.Assignee.name).findValueByLogin(ctx.currentUser.login)) { issue.fields.Assignee = ctx.currentUser; } }, requirements: { Assignee: { type: entities.User.fieldType } } });
Last modified: 19 April 2024