Over the past few months, we’ve released several products — including checklists, a handbook, and classes — to help new hires orient themselves to 18F.
By far the most successful onboarding item we’ve released is a Slack bot that sends scheduled messages to new hires so that they don’t experience information overload during their first week.
Mrs. Landingham, as the bot is affectionately called, drips out all kinds of information to new hires, like how to fill out forms, participate in discussions, and learn what certain terms mean. As the lead developer on Mrs. Landingham, I want to share how we developed the concept and built the bot, and detail where we plan to go next.
Developing the concept for the Mrs. Landingham MVP
In keeping with our agile methodologies, we started development of the Slack bot by discussing the absolute minimum feature set required to get Mrs. Landingham up and running.
The problem we were aiming to solve was simple: We wanted to prevent information overload for new 18F employees. The solution? We weren’t sure exactly, but Slack felt like the right medium for the job because:
-
Slack is the primary medium for communication at 18F.
-
Receiving a message on Slack feels more fun and immediate than other mediums, such as email, which would help with our goal of “not stressing out new employees.”
-
Developing a Slack bot would provide opportunities down the line for interesting experiments, such as allowing employees to reply to the bot with questions (and receive answers!).
-
We wanted a way to automate and then trickle out messages so that new hires were not overwhelmed during their first days and Slack direct messages (DMs) seemed like they would work well.
For the MVP, we decided that the simplest solution possible was to set up a small Rails app with a database for storing new employee Slack names, start dates, and scheduled messages. We decided to use Rails instead of a lighter framework because we also planned to create a frontend web interface for our 18F colleagues to add and edit information without needing to understand Ruby or Git.
Each scheduled message would have a “day” associated with it. One message would be for “day 1,” one for “day 2” and so on. Once per day, the app would run a job that would match employees and messages based on the “day” of the message and the start date of the employee and the Slack bot.
Naming the bot
Naming is a fun and important step for any project. 18F already had a Slackbot named “slackbot”, so that name was out. We started bouncing around names, from “Onb04rd1ngb0t3000” to “Siri” to “Joanne” — but nothing felt right.
Then, Greg Boone suggested “Landingham”, which was immediately greeted with a heart-eyes emoji reaction. There are many West Wing fans at 18F, and something felt right about bringing back the beloved character Dolores Landingham, who served as fictional President Bartlett’s wise and sometimes ornery secretary, to guide new employees through the turbulent waters of onboarding.
Voila! Mrs. Landingham
was born.
Building the bot
We’ll spend the next section talking about bot development. If you’d like to go directly to our future plans, skip ahead to the “A future for Mrs. Landingham” section.”.
In the Ruby section on the Slack integrations page, there are dozens of examples of Slack integrations, though none specifically addressed drip campaigns. (A drip campaign is when messages are sent to users based on a schedule or trigger event, such as number of days after being added to a Slack bot.) This meant we would have to build something from scratch rather than clone and edit an existing bot.
To handle our Slack API integration, we chose the Slack Ruby Client gem, a tool for handling Slack’s web and real time messaging APIs. To start, we added a new bot user to the 18F Slack organization and configured the gem using the API token for the bot user.
# Gemfile
gem 'slack-ruby-client'
group :test, :development do
gem 'dotenv'
end
# .env
SLACK_API_TOKEN='DOLORES_BOT_API_TOKEN'
Next, we started work on our database. The two database tables required for a drip campaign bot are employees
and scheduled_messages
. The bare minimum fields for employees
are slack_username
andstarted_on
: the first is so the bot knows who to direct message, and the second is so we know where in the drip campaign an employee is located.
# db/migrate/20150917233323_create_employees.rb`
class CreateEmployees < ActiveRecord::Migration
def change
create_table :employees do |t|
t.timestamps null: false
t.string :slack_username, null: false
t.date :started_on, null: false
end
end
end
# db/migrate/20150918175357_create_scheduled_messages.rb`
class CreateScheduledMessages < ActiveRecord::Migration
def change
create_table :scheduled_messages do |t|
t.timestamps null: false
t.string :title, null: false
t.text :body, null: false
t.integer :days_after_start, null: false
end
end
end
For our first iteration, we decided we would run a task once per day that would send out messages to employees. This meant iterating through each message and figuring out which employees should receive that message based on their start date:
# app/services/message_sender.rb
require "slack-ruby-client"
class MessageSender
def initialize
configure_slack
end
def run
ScheduledMessage.all.each do |message|
employee_slack_usernames_for_messages = find_employees(message)
employee_slack_usernames_for_messages.each do |slack_username|
channel_id = SlackChannelIdFinder.new(slack_username, client).run
client.chat_postMessage(
channel: channel_id,
as_user: true,
text: message.body
)
end
end
end
private
def configure_slack
Slack.configure do |config|
config.token = ENV['SLACK_API_TOKEN']
end
end
def find_employees(message)
MessageEmployeeMatcher.new(message).run
end
def client
@client ||= Slack::Web::Client.new
end
end
#app/services/message_employee_matcher.rb
Class MessageEmployeeMatcher
def initialize(message)
@message = message
end
def run
Employee.where(started_on day_count.days.ago).pluck(:slack_username)
end
private
attr_reader :message
def day_count
message.days_after_start
end
end
One of the trickiest parts of writing this code was figuring out how to send a message to a user as a direct message. Sending a direct message via the Slack API is exactly the same as sending a message, except that the ID of the channel is the ID of the direct message channel between the bot user and another Slack user. When sending a message to a regular channel, you can just use the name of the channel. For example, you could send the message “Hello world” to the general Slack channel of your organization like this:
client.chat_postMessage(
channel: "general",
text: "Hello world"
)
When you want to send a direct message to a user, you cannot just set the channel as their Slack username. Instead, you must open an “IM” (the Slack API method for opening a direct message is im_open
) with that user and find the ID of the IM channel. We wrote a small service class to do this:
# app/services/slack_channel_id_finder.rb
class SlackChannelIdFinder
def initialize(slack_username, client)
@slack_username = slack_username
@client = client
end
def run
slack_user_id = slack_user["id"]
chat = client.im_open(user: slack_user_id)
chat["channel"]["id"]
end
private
attr_accessor :client, :slack_username
def slack_user
all_slack_users.find { |user_data| user_data["name"] = slack_username }
end
def all_slack_users
client.users_list["members"]
end
end
Once we had our class that found the correct message to send to each employee, we needed a way to automatically run the task once per day. We chose Clockwork for the job:
# Gemfile
gem "clockwork"
# config/clock.rb
require "clockwork"
require_relative "boot"
require_relative "environment"
module Clockwork
every(1.day, "scheduled_messages.send", at: "19:30", tz: "UTC") do
puts "Sending scheduled messages"
MessageSender.new.run
end
end
Since we were deploying Mrs. Landingham to cloud.gov, we updated our manifest.yml
instructions to run script/start
:
# manifest.yml
domain: 18f.gov
command: script/start
# environment-specific configuration
applications:
- name: dolores-app
buildpack: https://github.com/cloudfoundry/ruby-buildpack.git
memory: 1GB
env:
DEFAULT_URL_HOST: dolores-app.18f.gov
RESTRICT_ACCESS: true
and set up script/start
to run foreman:
# script/start
#!/bin/sh
set -e
set -x
bin/rake cf:on_first_instance db:migrate
foreman start -p $PORT
and added a clock process to our Procfile
:
# Procfile
web: bundle exec puma -p $PORT -C ./config/puma.rb
clock: bundle exec clockwork config/clock.rb
If you are deploying an app with Clockwork to Heroku, the Clockwork README has a nice quickstart script.
A future for Mrs. Landingham
Once we established Mrs. Landingham’s basic functionality, we started creating content and onboarding employees with the messages. And it wasn’t just new employees who received the new messages. Employees who started working at 18F months or even years ago started requesting to be added to the bot, since the messages contained valuable information.
Soon enough, we realized small enhancements would improve the functionality even more. We added support for employee time zones and the ability to send messages multiple times per day. We also added tagging capabilities to make it possible for employees to subscribe to a certain type of message, such as “benefits” or “legalstuff,” without receiving every single message in the system.
Looking forward, we have a lot of exciting ideas for how Mrs. Landingham could be even more helpful to 18F’s employees, new and old. You can check out some of the ideas on GitHub.
Feedback
In addition to the positive reaction to Mrs. Landingham internally, we’ve heard from many other organizations that they would also like a Slackbot drip campaign for new employees. Like everything we build at 18F, Mrs. Landingham’s code is open source and available for anyone who would like to use or adapt it for their needs.
Mrs. Landingham’s primary contributors so far have been Brian Hedburg, Carlo Costino, Melody Kramer, and myself. We welcome any outside contributions and hope that others who find Mrs. Landingham useful will send code for exciting features our way so that we can all continue to create a better onboarding experience together.