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 call
is_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.