Rendering content in both Intercom and Slack

Lessons learned building Intercom’s new Slack app

While building our new integration between Slack and Intercom — which lets you reply to leads and users from Slack — one of our first considerations was how to represent content from Intercom in Slack. This question would be core to the user experience for our integration: we needed to ensure that responding to Intercom conversations from Slack wasn’t a broken experience.

If you’re rendering content in Slack from another platform or vice versa you might be interested in reading how we solved the problem.

First, some context

Intercom is a customer communication platform that lets businesses talk to customers almost anywhere: on their website, in their app, on mobile and via email. Many businesses use Intercom’s messenger on their website so that leads and customers can chat with them — for example, if someone has questions about pricing or is experiencing a bug.

Our two-way Slack integration makes it easier for sales teams to quickly reply to those conversations directly from Slack, saving them switching between tools and allowing them to respond to leads faster and then chat, qualify and close deals from the app they already have open all day.

The integration routes those conversations to user-specified Slack channels as notifications, from which the user can then reply. Clicking the Reply in Slack button creates a new Slack channel for the responder to chat with a lead directly, with everything syncing seamlessly between Slack and Intercom.

The challenge of rendering

In order to ensure the experience of responding to Intercom conversations from Slack felt natural and smooth, we had to find an elegant solution for rendering content between our concept of “conversations” in Intercom and the concept of “channels” in Slack.

For example, if a customer attaches a file in a conversation from the Intercom messenger, it needs to be accessible and rendered correctly for the Slack user who is replying to the conversation within a channel. The same goes for emojis, links, GIFs and so on. This meant we needed to carefully map how we’d transform Intercom’s conversation content to a Slack channel, and vice versa.

We built the integration using Intercom’s APIs and webhooks as a way to dogfood our own platform — our conversations content is in HTML. Some of the questions we needed to answer were:

  • Does Slack support HTML?
  • Is Slack markdown standard?
  • How are emojis, links and permalinks represented in Slack?
  • How would rendering media work?
  • How are files represented on Slack, and are they always private?
  • Are there best practices for message appearance in Slack?

Think big, start small

In Intercom, we tend to think big and start small. When we set to answer these questions, we thought of the holistic solution, and then started on the first “cupcake” solution.

Our prototyping version was a simple renderer class, that took content from Intercom, parsed the HTML document, applied some logic, and generated a Slack message with attachments, buttons and fields.

This solution was a good proof of concept and worked within the boundaries of a prototype, but we knew it wouldn’t scale well with the considerable amount of content types we see in Intercom. So, we set out to build a flexible, extensible, and testable pipeline that would work for our Slack integration, and other integrations in the future.

Going back to basics, we found that a combination of chain of responsibility and decorator patterns could scale here.

Building the pipeline

In Intercom, we have “Conversations” which contain “Conversation Parts” (the individual messages within a threaded Intercom conversation). Each conversation part has a type, like a status such as Open, Close, Assign, User Reply, Teammate Reply, etc. Text can be formatted, articles can be inserted, and conversation parts can be both chat messages or emails sent to Intercom’s inbox.

Each of these Conversation Parts has an author and body (and sometimes a subject). The integration posts these parts in a Slack channel, sometimes as part of a conversation and sometimes just as a notification.

We defined four concepts to serve as the foundation of the rendering pipeline:

  1. Renderer: receives a context object, and calls the matching Renderable
  2. Context: a container that contains objects a Renderable would need to render
  3. Renderable: renders, builds and produces a Slack API-compatible message
  4. Transformation: a transformation applies certain logic to parse and output Slack-compatible content

A Renderer can select and choose a Renderable based on the Context provided, in which the Renderable in turn applies one or more Transformation(s).

For example, a Note in Intercom (which is essentially an internal-only note in a conversation that is only visible to you and your teammates, and not your customer), will use the “PartRenderer” class, which will create a Note object and then call its render method. A simplified version could look like:

This is how a Note part looks like in Slack after the rendering:

The Note part handles its own rendering providing that it has the suitable “Context” passed to it. The “Context” could contain, the conversation part, author, body, assignee, etc.

Every part in principle goes through this cycle:

  1. Receive “Context”
  2. Parse HTML body provided (using Nokogiri)
  3. Run a series of defined transformations
  4. Build and return a Slack message with attachments

A quick look at the Note part skeleton could look like:

The Note part (and other parts) don’t have to redefine most of the rendering logic as it is common between many part types. Instead, the Reply class (base class) can do that job.

Five tips to make rendering easier

Whilst building this pipeline, we learnt some tips and tricks that might be useful if you need to consider rendering in your own integration:

1. Slack Markdown has some differences to standard Markdown. For example, a bold would be single asterisk, instead of double. We wrote a transformation for content format — a simplistic version using Nokogiri to replace headers with bold text could look like:

2. Slack shared files are by default private, though an attachment file needs to be accessible to Intercom’s end user when it is shared in a Slack channel that is mapped to an Intercom conversation. A little trick helped by using the files.sharedPublicURL method.

3. Images (or gifs) can be shared in Intercom’s messenger, and we wanted to make sure these images could be easily previewed in Slack. We used the image_url attribute in a Slack message attachment:

4. The chat.update method is useful for ensuring that the message posted on Slack is always update to date and with relevant context, and the Slack message fallback attribute was also helpful for ensuring Slack notifications have the right content.

5. We found the slack-markdown Ruby gem useful to render markdown content from Slack to Intercom, and we extended it with a couple of extra transformations too.

Extending slack-markdown gem Processor class and adding more filters:

In the end, we ended up with a successful integration that renders content seamlessly between Intercom and Slack.

an example of rendering a reply between Intercom and Slack

Hopefully, our learnings from this process will help inform your own decisions about rendering content in your integration, and save you some time along the way!