Introducing our most accurate /search yet. Read the announcement →

How to Build an AI SDR that Researches Companies in Real Time

placeholderBex Tuychiev
Jun 01, 2026

TL;DR

  • An AI SDR is software that researches potential customers and writes the first outreach message. This article builds an agent that does the research live, at send time, instead of reading from a stale list.
  • The stack: Firecrawl web search (/search) for fresh signals, Firecrawl's scrape endpoint with a JSON schema for structured company intel, OpenAI for the writing, Python to wire it together.
  • "Real time" means the signals (a funding round, a launch, a job post) are pulled off the live web the moment the agent writes, so they're current.
  • You end with a script that runs the same research-and-write loop from 1 company to 50+ concurrently.

A sales rep can't research 100 accounts a day. So message creation is defaulted to a template, and templates get ignored.

The numbers back that up. The average B2B cold-email reply rate fell to 5.8% in 2024, down from 6.8% the year before, across 16.5M emails (Belkins, 2025). Personalized emails reply at 17% against 7% for generic ones, a 2.4x difference, across 20M+ sends (Woodpecker). The difference comes from the specificity, and specificity costs research time.

Research time is the part you can automate. This guide builds an agent that researches each company before it writes a word, using Firecrawl web search to read the live web. We point it at one company, watch it run, then scale the same loop across a list.


What is an AI SDR?

An AI SDR (sales development representative) is software that finds potential customers, researches them, and writes the first outreach message. It does the early work a junior rep would do, up to the point of starting a conversation, not closing the deal. It's one of the higher-leverage AI automation examples teams are shipping today.

Most AI SDR tooling personalizes from static data, like CRM fields or a contact list scraped once when it was built. Only about 25% of teams use any intent or signal tooling at all (Autobound, 2026, vendor-sourced and directional). The rest write from whatever was true the day the list was made. For building that initial contact list, see our guide on sales lead extraction in Python.

The problem with static personalization is that signals decay. A funding round, a product launch, a new VP of Engineering job posting are the hooks that make a first line land, and they're only worth citing while they're fresh. A list built three months ago has none of them.

A two-lane comparison showing a static AI SDR pulling from a frozen snapshot versus a real-time AI SDR reaching out to the live web for fresh signals at send time.

The agent we are going to build researches live, per company, at the moment it writes, so the information is always fresh.

How to build an AI SDR

Setup and prerequisites

Firecrawl gives AI agents fast, reliable web context through search, scraping, and browser interaction tools. This agent uses two of those: web search to read the live web, and scraping to read a company's own site. OpenAI writes the message.

For this project, you need Python 3.10+, a Firecrawl API key, and an OpenAI API key.

Install the SDKs:

uv pip install firecrawl-py openai pydantic python-dotenv
# or: pip install firecrawl-py openai pydantic python-dotenv

firecrawl-py and openai are the two API clients. pydantic defines the shape of the data we pull from each site, and python-dotenv reads your API keys from a file so they stay out of the code.

Put both keys in a .env file at the project root:

FIRECRAWL_API_KEY=fc-...
OPENAI_API_KEY=sk-...

Set up the API clients:

import os
from dotenv import load_dotenv
from firecrawl import Firecrawl
from openai import OpenAI
 
load_dotenv()
 
firecrawl = Firecrawl(api_key=os.environ["FIRECRAWL_API_KEY"])
openai = OpenAI(api_key=os.environ["OPENAI_API_KEY"])

load_dotenv() reads the .env file into environment variables, and the two clients pick up their keys from there. The rest of the code in this guide uses these firecrawl and openai objects directly.

The agent runs as four stages, one per section below:

  1. Search pulls live signals off the web (Firecrawl /search).
  2. Extract pulls structured identity off the company's own site (Firecrawl scrape with a JSON schema).
  3. Write hands both to OpenAI and gets a message.
  4. Batch runs the whole loop across a list.

A left-to-right four-stage pipeline (search, extract, write, batch) with the two Firecrawl-powered research stages highlighted in orange and the writing and scaling stages in white.

Search and extract are the Firecrawl stages, where the web context comes from. One company or fifty, the four stages don't change.

(For a broader look at how AI agents work and why web context matters, see our guide to AI agents.)

How do you search for live company signals?

Firecrawl /search endpoint is web search built for agents: you give it a query and get back ranked live results with the page content already pulled, in one call. It's the stage that makes the agent "real time," because it reads the web as it is today.

The query matters most here. You want recent events worth mentioning in a message, not the company's About page. A plain query plus a recency filter gets you there.

def find_signals(company: str, limit: int = 5) -> list[dict]:
    """Search the live web for recent, outreach-worthy signals about a company.
 
    `tbs="qdr:m"` scopes results to the past month so we get fresh hooks
    (funding, launches, hiring) instead of evergreen background pages.
    """
    results = firecrawl.search(
        query=f"{company} latest news",
        limit=limit,
        tbs="qdr:m",  # past month, so signals are fresh
    )
    return [
        {"title": r.title, "url": r.url, "summary": r.description}
        for r in (results.web or [])
    ]

Two things to know about this snippet. firecrawl.search() returns an object with the results under results.web, which can be None if nothing matched, so the or [] keeps the loop from breaking on an empty search. Each result r has .title, .url, and .description, and those three are all we need to feed the writer.

The tbs="qdr:m" argument is the recency filter. It limits results to the past month, which surfaces recent events over a company's older pages. Change it to qdr:w for the past week if a company moves fast. Keep the query itself plain too: "{company} latest news" works, while stacking operators like funding OR launch OR hiring on top of the recency filter often returns nothing.

Run the script to research one company:

python ai_sdr.py

Calling find_signals("Tailscale") returns the live results. Here are two of them, each with the title, URL, and summary that become a signal:

[
  {
    "title": "How to backup tailscale configuration on Linux host - Reddit",
    "url": "https://www.reddit.com/r/Tailscale/comments/1tlvq3q/...",
    "summary": "I recently had the nightmare scenario of corrupting the Linux installation on the host I use for an exit node..."
  },
  {
    "title": "Canada's Bill C-22 and the security cost of collecting more data",
    "url": "https://tailscale.com/blog/bill-c22-canada",
    "summary": "Canada's Bill C-22 risks forcing secure services to retain more metadata and build access systems. Tailscale explains why the bill should change."
  }
]

The title and summary are the hook. A blog post on Bill C-22 or a fresh security bulletin is something a rep can open a message with, and both are dated to this month. This is the volatile half of the research: events that are only worth mentioning while they're current.

How do you extract structured company intelligence?

This step reads what a company is: what it sells, who it sells to, how it describes itself. That identity lives on the company's own homepage, so you scrape it.

The scrape endpoint pulls a single page, and with a JSON schema it returns that page as a typed object instead of raw text. You hand Firecrawl a Pydantic model, and Firecrawl fills the fields from the page.

Start with the schema. Keep it to what a sales rep would skim a homepage for before writing a first line:

from pydantic import BaseModel, Field
 
 
class CompanyIntel(BaseModel):
    name: str = Field(description="The company's name as it presents itself.")
    products: list[str] = Field(description="The main products or services it sells.")
    sells_to: str = Field(description="Who it sells to, in one short phrase.")
    positioning: str = Field(description="How it describes itself in one sentence.")
    pain_points: list[str] = Field(description="The customer problems it claims to solve.")

The description on each field is the instruction Firecrawl follows when it fills that field, so each one reads like a note to a researcher. Then the extract step: