Outr.ai

Published Oct 12, 2024
Updated Nov 20, 2024
4 minutes read

Background

Every sales organization uses cold email outreach to generate and qualify leads. With Google adding stricter spam policies to Gmail, this becomes increasingly difficult to maintain positive domain ratings.

Amidst all the Gmail chaos, Bridger Tower and I started to notice that sales teams were starting to send from multiple domains. That way, if a domain rating went south, they could dump it and move on to a new one.

Seeing this, we saw a market for creating a tool that allowed multi-domain sending along with automated outreach powered by AI.

Tech Stack

  • Next.js
  • TypeScript
  • React & RSC
  • ReactFlow
  • Zustand
  • Next Safe Action
  • Postgres
  • Vercel
  • Cloudflare Workers
  • AWS SES
  • SST
  • Custom Mail Servers
  • shadcn/ui
  • Upstash QStash, Redis, Workflows
  • Our fork of Maily.to / TipTap

App Overview

Outr.ai is divided into a few parts:

Contacts

A user can either upload a CSV of contacts or add them manually.

Outr.ai Contacts

Sequences

Users create sequences which make up the template for a workflow. Sequences are made up of AI Nodes, Email Nodes, and Wait Nodes. There is a templating system built into the sequence builder so that a user can pass field values from the contact info into the builder. Built in validations are an immediate feedback loop to ensure users build usable workflows.

Outr.ai Sequence Builder

Domains

Users can configure many domains that they can send and receive emails from. After adding the domain, users are given the various DNS records to add. This allows Outr.ai to send and receive emails on their behalf—fully managing the email infrastructure.

Outr.ai Domains

Campaigns

With a sequence created and a domain configured, the user creates a campaign by selecting the sequence and sending domain.

Outr.ai Campaign Creation

The user can then add contacts to the campaign. Once contacts are added, AI scrapes the site of each contact and writes personalized emails based on the user's brand & product offering.

Outr.ai Campaign Contacts

After the AI emails are written, the user has the option of viewing and editing the emails before sending.

Outr.ai Campaign Editing

Once the workflow is published, the emails will send to contacts based on the sequence structure previously created. The executed steps can be viewed individually for each contact.

Outr.ai Campaign Execution

Replies

If the campaign goes well, contacts will reply to the emails and express interest. When a reply to one of the outreach emails is received, it can be seen in the Replies tab of the application. This is essentially a cross-domain inbox for all email replies.

The user can also reply directly within this interface and it will send from the original sending domain.

Outr.ai Replies

Profiles

Finally, profiles allow the user to create different company / product profiles to give context to the AI. This is to give proper context when writing emails about your brand or offering.

Outr.ai Replies

Technical Details

Workflow

One of the key pieces of the app is the workflow builder. We opted to use ReactFlow with Zustand. Zustand greatly reduced the state management complexity in comparison to the React Context API. A big challenge in building a graph-like workflow builder was validating and parsing the workflow. To do this, we created various helper functions that used common graph traversal algorithms like breadth-first search to quickly reconstruct the workflow and validate it.

For the durable execution provider, we ended up going with Upstash. The API was very simple and it allowed us to integrate quickly into our Next.js application. After parsing the workflow and constructing a usable structure, we created a single route.ts file that executed the workflow.

Below is a very simplified version of the route.

export const POST = serve<WorkflowPayload>(async (context) => {
  let currentIndex = 0;
  while (currentIndex < parsedFlow.length) {
    const currentAction = parsedFlow[currentIndex];
    switch (currentAction.type) {
      case "email": {
        await context.run(`send email: ${currentAction.id}`, async () => {
          // send email
        });
        break;
      }
      case "ai": {
        await context.run(`send ai email: ${currentAction.id}`, async () => {
          // send ai email
        });
        break;
      }
      case "wait": {
        await context.sleep(// sleep);
        break;
      }
      default: {
        break;
      }
    }
    // Move to the next action
    currentIndex++;
  }
});

Email sending

The next challenge was setting up a way to send emails while maintaining good deliverability and domain management. After much consideration of platforms like Resend, Twilio, Mailgun, etc. we decided to build our own custom solution on AWS SES. This allowed the most flexibility and ensured that we had low-level access to necessary primitives like message headers and message IDs. AWS SES also provides a solid API for adding and configuring domain indentities which was vital for this project.

SST allowed us to easily create a TypeScript API for sending emails.

The most difficult part of managing emails was not in the sending but was actually in the email receiving. After A LOT of research, trial and error, and luck we landed on a solution. We use a custom mail server to route emails to a catch-all domain. When a user configures a domain, they add a few MX records that essentially forwards emails to our catch-all email address. This catch-all email address is a trigger for a Cloudflare Worker that then parses the email and triggers a webhook in our core application that saves it to the database and notifies the user.

AI Tasks

Scraping thousands of websites can be very computationally intensive and expensive, especially when using AI to parse, organize, and write information. To solve this issue we used a combination of caching and background jobs. For this we also used Upstash for Queues and Redis. When contacts are added to campaigns, a task is queued to scrape the website of the contact and write an email. This scraped information is cached in Redis so that if the user later writes an email for the same contact, we don't have to do another expensive operation.

Email WYSIWYG

When building an email editor, we initially used Maily.to as is. However, we started to run into issues where we needed more custom solutions. Maily.to is great open source software and much of what we needed was already done for us. We ended up creating a fork of the project for use on Outr.

Instead of React Email, we parse the editor as plain HTML. This allows us to easily render the emails to preview. It also allows us to easily swap data into the email content with our custom built macros. There are some quirks with the way we're doing it, but Maily.to and TipTap made it a lot easier to do this. We're also excited that we can later extend Maily.to and TipTip to have custom plugins for added email content.

Wrapping Up

I really think Outr.ai pushes the boundaries of what is being done with cold email infrastructure, especially when combined with AI. There are a number of issues we're still solving for, but it's been fun to solve problems that not many people have encountered before.

If you have any ideas on how to improve Outr, please hit me up on Twitter/X!