How to programmatically send personalized emails to 200 decision makers of Fortune 500 companies (with code samples)

November 9, 2020

8 min read

I want to close bigger deals by reaching decision-makers directly I am the founder of Enrich Layer. I lead sales for Enrich Layer, and I do this alone. So it is...

I want to close bigger deals by reaching decision-makers directly

I am the founder of Enrich Layer. I lead sales for Enrich Layer, and I do this alone. So it is vital that whatever I do is leveraged and effective. Luckily, I can code.I figured that general emails do not get to decision-makers in large(r) companies, and I want to move up the value chain. I want to close bigger deals. I think I can achieve that by reaching decision-makers directly and with emails personalized with a unique problem-solution statement for their company. To say, I want to do what other sales reps are doing and more.To be very specific, I want to reach 200 decision-makers at a single burst. I want to send 25 personalized follow-ups per decision-maker.In this blog post, I will share how I accomplished this with code.

Getting a list of companies with Crunchbase Pro

I want to target larger companies that have the budget to purchase Enrich Layer to build data-driven products. In particular, I have shortlisted companies that belong to the likes of sales automation tools, job boards, and talent sourcing companies to be our target market. Crunchbase Pro is excellent for building a list like such.

With Crunchbase Pro, I started a Company Search for a list of companies that

  • matched Enrich Layer's target industries
  • have revenues that are more than 5+M per annum

Then, I exported the search results into a CSV file, ensuring that I have a column of data that includes the company's professional networks Profile.

Once I have the list, I want to enrich the data with company names and their corresponding corporate website. This is the Python script I used to enrich data for the list of companies I had exported from Crunchbase Pro:

`async def get_company(company_profile_url: str): api_ Social Network/company' header_ _ in range(RETRY_COUNT): try: async with httpx.AsyncClient() as client: client.get(api_endpoint, company_profile_url}, assert r.status_ 200 return r.json() except: continue

return None

async def enrich_companies(lis): for profile_url in lis: get_company(profile_url) if coy is None: return None None) coy_ None)

todo - (task for reader) save and in a file

`

Find decision makers with Enrich Layer API

Now that I have companies, I need decision-makers. Decision-makers in this exercise mean people in the roles of CEO, COO, CTO, and VP of Product.

To accomplish this, I will search for them on Google. For example, if I want to find the professional networks profile of the CEO of Cognism, I will enter the following search phrase in Google:

professionalsocialnetwork.com/in ceo cognism

Chances are, the correct profile will be in the search result. I will then repeat the query with different roles till I have a list of profiles. And it works.

To perform Google searches at scale, I will use Enrich Layer's "Crawling other pages" endpoint. This is how I programmatically make Google Search queries with Enrich Layer:

`async def google_search_async(search_term, retry_ -> List[str]: """ Perform a Google Search via Overlord and return a list of results in terms of URLs in the first page. """ for _ in range(retry_count): try: search_ search_url, "type": 'xhr', }

async with httpx.AsyncClient() as client: client.post(f"{OVERLORD_ENDPOINT}/message", OVERLORD_PASSWD), if r.status_code != 200: print( f"Google search failed with , retrying.") assert r.status_ 200

html_ result_ a[ping]") href_ for result in result_lis: if '//webcache.googleusercontent.com/search' in href: continue if 'https://translate.google.com/translate' in href: continue href_lis += [href] if len(href_lis) == 0: continue return href_lis except: traceback.print_exc() continue raise Exception However, my computer is not smart enough to understand when a CEO is the same as "Chief Executive Officer." Or that "Engineering Head" and "Chief Engineering" are very much alike. For that, I have an algorithm which I callis_string_similar()`. You can find the algorithm to check if two strings are similar [here](https://giki.wiki/@Enrich Layer/Software-Engineering/similar-string).

Once I have a list of user profiles, I need to ensure that:

  • The profile's current employment belongs to the company that I am googling for (Google gets this wrong sometimes)
  • The profile's current role at the company matches the decision making roles.

To perform the checks above, I will:

  • Enrich the user profiles with Enrich Layer's Person Profile Endpoint to get the profile's list of experiences.
  • Verify that his/her active employment matches up.

This is how I accomplish the above in Python code:

`async def get_person_profile(profile_url): api_ Social Network' header_ _ in range(RETRY_COUNT): try: async with httpx.AsyncClient() as client: client.get(api_endpoint, profile_url}, if r.status_ 404: return None assert r.status_ 200 return r.json() except: continue

print(f"{profile_url} retried {RETRY_COUNT} times but still failing") return None

async def google_search_async(search_term, retry_ -> List[str]: """ Perform a Google Search via Overlord and return a list of results in terms of URLs in the first page. """ for _ in range(retry_count): try: search_ search_url, "type": 'xhr', }

async with httpx.AsyncClient() as client: client.post(f"{OVERLORD_ENDPOINT}/message", OVERLORD_PASSWD), if r.status_code != 200: print( f"Google search failed with , retrying.") assert r.status_ 200

html_ result_ a[ping]") href_ for result in result_lis: if '//webcache.googleusercontent.com/search' in href: continue if 'https://translate.google.com/translate' in href: continue href_lis += [href] return href_lis except: traceback.print_exc() continue raise Exception

async def find_people_in_roles(coy_name: str, li_coy_profile_url: -> List[str]: MAX_ 'cto', 'coo', 'vp engineering' ]

def does_role_match(role: str, person_profile: Dict) -> bool: for exp in person_profile['experiences']: if not (exp['ends_at'] is None and util.is_string_similar(coy_name, exp['company'])): continue

if not util.is_string_similar(role, exp['title']): continue

return True return False

async def search_li_profile(role: str) -> List[str]: url_result_ google_search_async(f"professionalsocialnetwork.com/in {role} {coy_name}", retry_ profile_url_ x: 'professionalsocialnetwork.com/in' in x, url_result_lis)) return (role, profile_url_lis)

print("Performing google search for user profiles") for role in ROLES] search_ asyncio.gather(*tasks)

profile_url_ for _, profile_lis in search_results: for profile_url in profile_lis: if profile_url not in profile_url_lis: profile_url_lis += [profile_url] print(f"Total of profiles to query")

profile_ working_ for idx, profile_url in enumerate(profile_url_lis): working_lis += [profile_url]

if (idx > 0 and len(working_lis) > 0 and idx % MAX_ 0) or (len(profile_url_lis) - 1): print(f"Working on profiles..") for url in working_lis] profile_data_ asyncio.gather(*tasks) for idx, url in enumerate(working_lis): profile_dic[url] = profile_data_lis[idx] working_

print("Done crawling profiles")

save data somewhere

` And now, I have a list of user profiles of decision makers.

Get email addresses of decision-makers

There are two parts to this problem. The first part is in getting an email address. The second part is in verifying the email address.

In getting email addresses, I use Clearbit because they have an API to resolve a domain name and a name into an email address.

And while Clearbit is good at fetching email addresses with high accuracy, their data is quite stale. My experience is that up to 30-40% of email addresses fetched from Clearbit bounce. And it's terrible for my cold outreach domain's reputation. Cold emails don't work if my emails end up in Spam. Mine does not because I make sure my emails are personal, and they do not bounce.

To verify email addresses that I retrieve from Clearbit, this is how I do it:

def verify_email(email): api_ return resp['result'] == 'valid' and resp['status'] == 'success' With this, I have a list of email addresses of decision makers from my list of target companies.

Send sequenced cold emails

I have tried sending templated one-liner emails, and while they did work, I wanted better hit-rates from my emails. Everything that I have set out to do so far can be automated. But the one thing I cannot automate is understanding my target company's business; figuring out how Enrich Layer's API can fit into their product; writing it down in clear, simple problem-solution statements in 25 personalized email templates.

So while my emails are templated, 80% of the content in my emails are variables customized by my input. And this is how I do it.

I create a simple web app that iterates through the list of prospects to personalize an email for that specific company. For 200 decision-makers, this took me a few hours going through about a hundred companies. Personalizing emails was the worse part of the process.

Did it work?

Unfortunately, I got better results with one-liner cold email outreaches to general emails. Here is my take-away:As smart as I thought I was, I think I overreached. I was overly verbose with the problem-solution statement because the first paragraph of the email was 3-4 lines long. Secondly, I am wrong in assuming that CEOs or COOs cared about Enrich Layer. Enrich Layer is, after all a solution to be vetted by a developer/product team and not the CEO.I am not giving up. Next up - I am crawling developers of product-oriented companies and reaching out to them directly with simple short-liners. I hope it works better, and I will keep you updated in a follow-up post.