Best Practices How-To's

Make It Workflow — Part 9: Broadcasting Bulletins

In previous posts, we have described improvements for popular use cases that you would expect to be supported in an issue tracker. In addition to standard features like time tracking and helpdesk automation, YouTrack is capable of supporting really unusual scenarios with workflows. In this post, we look at how to use YouTrack as a platform for sending email newsletters.

blog_10@2x

Bulletin Backstory

Let me tell you how this particular use case was born. One of my responsibilities at JetBrains is to analyze and support business processes for operational teams that include accountants, legal counsels, and travel coordinators. These teams use YouTrack to organize their work and communicate with other employees in our organization. Sometimes, the changes I apply in YouTrack alter their day-to-day routine. Before I update their projects, I need to send them a bulletin that explains how the change impacts their process.

I ran into a situation where I needed to notify all of my colleagues who have access to the project that is used by the Accounting team. These changes affected nearly a quarter of our employees around the globe and none of the existing mailing lists included all of them. At the same time, all of the affected users were members of at least one of four groups in our YouTrack installation, and these groups are updated quite often.

So, I thought, if YouTrack has the most accurate information and the means to send an email message, why not use YouTrack to dispatch the bulletin?

Now, I’m not suggesting that you use YouTrack to send newsletters to external users as a replacement for your email marketing platform, but it can be used as a tool for managing internal communication.

Sending the Newsletter

My solution was simple. I created a dedicated project with a minimum number of fields, including State and Recipients (a list of groups). When the State becomes Sent, an email message that contains the issue summary and description is sent to every member of these groups.

The naive approach would be to send these emails using the `UserGroup.notify(subject, body)` method. However, this solution is not optimal. People who belong to multiple groups in the list will get spammed with several identical messages. Instead, I use `notifications.sendMail(message, issue)` and explicitly calculate the union of the groups in a list (meaning, a list of users that belong to at least one of these groups). With this approach, each recipient receives only one message. The newsletter author is the primary recipient and all others receive a carbon copy.

The first script we implement in this scenario collects all of the users from these groups to build a single mailing list (the script is called ‘filter’):

exports.getUniqueEmails = function(groups, firstEmail) {
  var userEmails = [firstEmail];
  groups.forEach(function(group) {
    group.users.forEach(function(user) {
      var email = user.email;
      if (email && userEmails.indexOf(email) < 0) {
        userEmails.push(email);
      }
    })
  });
  return userEmails;
};

A second utility script contains a function that prepares the message to be sent (its name is ‘composer’):

exports.composeMessage = function(issue, emails) {
  var subject = issue.summary;
  var body = issue.wikify(issue.description);
  var link = '<a href="' + issue.url + '">' + issue.id + '</a>';
  var footer =
    '
<div style="color: #777777;">' + 
    'This newsletter is delivered by YouTrack. You may find the content ' + 
    'of this newsletter anytime at ' + link + '.' + '</div>
'; 
  return { 
    fromName: issue.reporter.fullName, 
    toEmails: emails, 
    subject: subject, 
    body: body + footer 
  }; 
};

Now, armed with these two little utilities, we write a rule that sends an email message when the State changes to Sent:

var entities = require('@jetbrains/youtrack-scripting-api/entities');
var notifications = require('@jetbrains/youtrack-scripting-api/notifications');
var workflow = require('@jetbrains/youtrack-scripting-api/workflow');
var composer = require('./composer');
var filter = require('./filter');

exports.rule = entities.Issue.onChange({
  title: 'Send email to recipients',
  guard: function(ctx) {
    var issue = ctx.issue;
    return issue.isReported && issue.fields.becomes(ctx.State, ctx.State.Sent);
  },
  action: function(ctx) {
    var issue = ctx.issue;
    
    workflow.check(issue.reporter.login === ctx.currentUser.login,
      'Only ' + issue.reporter.fullName + ' can send this newsletter!');
    
    var emails = filter.getUniqueEmails(issue.fields.Recipients, issue.reporter.email);
    var message = composer.composeMessage(issue, emails);
    notifications.sendEmail(message, issue);
    
    issue.fields.Recipients.forEach(function(group) {
      issue.permittedGroups.add(group);
    });

    var newComment = issue.addComment('Newsletter was sent to ' +
      emails.length + ' recipient(s).');
    newComment.permittedUsers.add(issue.reporter);
  },
  requirements: {
    State: {
      type: entities.State.fieldType,
      Sent: {}
    },
    Recipients: {
      type: entities.UserGroup.fieldType,
      multi: true
    }
  }
});

Setting up a Safety Net

I’m usually quite cautious when it comes to writing workflows in a safe manner. For this reason, I added a couple of test actions. The first action rule helps me ensure that the email is sent to the right people:

var entities = require('@jetbrains/youtrack-scripting-api/entities');
var filter = require('./filter');

exports.rule = entities.Issue.action({
  title: 'Dump all emails to a private comment',
  command: 'newsletter-dump-emails',
  guard: function(ctx) {
    return ctx.issue.isReported &&
      ctx.issue.reporter.login === ctx.currentUser.login;
  },
  action: function(ctx) {
    var issue = ctx.issue;
    var emails = filter.getUniqueEmails(issue.fields.Recipients, issue.reporter.email);
    var text = 'Newsletter will be sent to ' + emails.length + ' recipient(s):\n\n' +
      '```\n';
    emails.forEach(function(email) {
      text += email + '\n';
    });
    text += '```\n';
    var newComment = issue.addComment(text);
    newComment.permittedUsers.add(issue.reporter);
  },
  requirements: {
    Recipients: {
      type: entities.UserGroup.fieldType,
      multi: true
    }
  }
});

The second action rule sends a copy of the message to my email account so I can preview the content:

var entities = require('@jetbrains/youtrack-scripting-api/entities');
var notifications = require('@jetbrains/youtrack-scripting-api/notifications');
var composer = require('./composer');

exports.rule = entities.Issue.action({
  title: 'Send test email (to the reporter only)',
  command: 'newsletter-test-email',
  guard: function(ctx) {
    return ctx.issue.isReported &&
      ctx.issue.reporter.login === ctx.currentUser.login;
  },
  action: function(ctx) {
    var issue = ctx.issue;
    var emails = [issue.reporter.email];
    var message = composer.composeMessage(issue, emails);
    notifications.sendEmail(message, issue);
  }
});

I also wanted to be sure that no one sees my work in progress before I’m ready to dispatch the newsletter, so I added a rule that hides the issue when it is just reported. The following rule restricts the issue visibility to its reporter:

var entities = require('@jetbrains/youtrack-scripting-api/entities');

exports.rule = entities.Issue.onChange({
  title: 'Set "Visible to" user on submit',
  guard: function(ctx) {
    return ctx.issue.becomesReported;
  },
  action: function(ctx) {
    ctx.issue.permittedUsers.add(ctx.issue.reporter);
  }
});

If you have read the previous posts in this series, you may have noticed that for each use case there is always room for improvements. This scenario is no exception. Here are some enhancements that you might consider:

  • Making new issues visible to a group of users who can collaborate on the newsletter.
  • Scheduling delivery based on the value that is stored in a field that stores the date and time.
  • Generating issues on a set schedule that include a template for content and a predefined send date if you deliver internal newsletters on a monthly or quarterly basis.

This will be our last installment in the ‘Make It Workflow’ series for the near future. This doesn’t mean that the party is over. We invite you to share your questions, comments, and ideas for new posts here or in our YouTrack Workflow Community channel in Slack.

Even when our blog is quiet, there are other sources of workflow-related information you can check for a steady stream of updates:

  • Watch our YouTrack Custom Workflow Repository in Git. We will continue to upload workflows for novel use cases to the repository and invite you to upload your own examples.
  • Read the YouTrack documentation. This resource is updated with each YouTrack release. The latest version includes an overview of the changes that were made to support ES6/JS2015 for workflows in JavaScript and the extensions to the workflow API that support VCS changes.