Heroku to Slack with AWS Lambda

When my production app processes change state on Heroku, I want to be notified in Slack:

Screenshot of Slack notification

Other examples:

17:38:52 clock.1 `bundle exec ruby schedule/clock.rb` up
17:39:03 web.1 `bundle exec puma -p $PORT -C ./config/puma.rb` up
17:39:05 web.2 `bundle exec puma -p $PORT -C ./config/puma.rb` up

Pager-notifying events:

17:38:52 queuenote.1 `bundle exec ruby queue/note.rb` crashed

Heroku has webhooks for these events but their payloads aren't in the format needed for Slack incoming webhooks.

AWS Lambda is the perfect glue to transform the Heroku webhook's JSON payload into a useful JSON payload for Slack's incoming webhook.

Slack config

Create an incoming webhook. Copy the URL.

Lambda config

Create a Lambda function. AWS' supported runtimes include Node, Python, Ruby, and Go. You can alternatively implement a custom runtime. Here's an example in Ruby:

require "json"
require "net/http"
require "time"
require "uri"

def lambda_handler(event:, context:)
  json = JSON.parse(event["body"])
  puts json

  if !["dyno", "collaborator"].include?(json["resource"])
    return {
      statusCode: 400,
      headers: {"Content-Type": "application/json"},
      body: "webhook event type not supported"

  localtime = Time

  if json.dig("resource") == "dyno"
    # https://devcenter.heroku.com/articles/webhook-events#api-dyno
    name = json.dig("data", "name") || ""
    state = json.dig("data", "state") || ""

    ignored_states = ["starting", "down"].include?(state)
    term_one_off = state == "crashed" && ["scheduler", "run"].any? { |p| name.include?(p) }

    if ignored_states || term_one_off || name.include?("release")
      return {
        statusCode: 200,
        headers: {"Content-Type": "application/json"},
        body: "ok"

    text = [
      "`#{json.dig("data", "command")}`", # backticks for code formatting in Slack
    ].compact.join(" ")

  if json.dig("resource") == "collaborator"
    # https://devcenter.heroku.com/articles/webhook-events#api-collaborator

    text = [
      json.dig("actor", "email"),
      "#{json.dig("action")}'d collaborator",
      json.dig("data", "user", "email")
    ].compact.join(" ")

  uri = URI.parse(ENV.fetch("SLACK_URL"))
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true

  req = Net::HTTP::Post.new(uri.request_uri)
  req["Content-Type"] = "application/json"
  req.body = {text: text}.to_json

  res = http.request(req)

    statusCode: 200,
    headers: {"Content-Type": "application/json"},
    body: res.body

Paste the Slack incoming webhook URL as an environment variable, which is encrypted at rest.

Create an API Gateway to make the Lambda function accessible in the Heroku web UI.

Heroku config

Go to:


Create a webhook with event type "dyno". Paste the API Gateway URL as the Payload URL.

Modify to taste

Edit and save the code in Lambda's web-based text editor. Trigger a webhook to test the function. View the auto-created CloudWatch logs for each function call.