Build a multi-agent GTM intelligence system
Brendan Short, who has spent years building GTM systems at companies like Zoom, made a pointed observation about re-engagement pipelines.
Most teams wait nine months after a lost deal before reaching out again, but the right moment isn’t calendar-based.
Instead, it’s when something just changed at that company, say a new VP joined, a funding round closed, a leadership hire landed in the right function.
The problem is that the signal and the timing don’t live in the same place. Who just joined a company and what that company announced recently reside in different records.
Seltz is a web index built for exactly this. Its people and news scopes return full structured records in a single call, so the join happens in one pass instead of being assembled from fragments.
Here’s how this helps:
One call per scope instead of a chain of search, fetch, and parse steps.
Full structured records back, not fragments you reassemble into an answer.
This article is a hands-on walkthrough of building a multi-agent GTM research pipeline with Seltz. But before the build, let’s understand why this class of question is harder than a standard search.
The join problem
Most search queries have one answer that can be found in one place.
“Who is the CEO of Nvidia” returns one record, “What did OpenAI announce last week” returns one article, and the agent moves on.
GTM research doesn’t work that way. An outreach agent needs to find which companies on a target list hired someone into a data or AI leadership role last quarter and what just happened there that makes now the right moment to reach out.
That answer doesn’t exist as a single page anywhere on the web. It only exists once you read a person’s full role history, cross-reference the company’s recent news, and merge both.
A standard search API does not return that record. It returns a ranked list of links, each with a snippet, meaning the two or three lines of text pulled from around the keywords that matched on the page.
A snippet is enough to tell you a page is worth opening and never enough to answer from. So the agent fetches the page, parses the HTML, pulls out the fields it needs, and repeats that for every person and every company on the list.
When each call returns a full structured record instead, the join becomes a merge across two complete records rather than an assembly job across fragments.
That’s the problem this pipeline is built to solve.
How the pipeline works
The pipeline runs three agents in sequence, each with a focused job.
Agent 1, the Signal Hunter, queries the Seltz news scope for recent trigger events at a list of target companies. It looks for leadership hires, funding rounds, product launches, or expansion announcements within your specified time window.
Agent 2, the People Enricher, takes the names surfaced by Agent 1 and queries the Seltz people scope for each one. It retrieves the full career record, every role with dates, prior companies, and education.
Agent 3, the Outreach Strategist, receives both outputs and merges them. It produces a ranked list of contacts, each with the trigger event, the full background, and a first outreach line written around the specific signal.
Neither Agent 1 nor Agent 2 fetches a second page. Each Seltz call returns a complete document, so the join in Agent 3 is a merge across full records, not a reconstruction from fragments.
Building it
We use CrewAI for orchestration, OpenRouter as the LLM provider, and the Seltz MCP server as the retrieval tool for all three agents.
The MCP integration means any agent in the crew can call Seltz through a single connected tool, no custom wrappers needed.
Setup
pip install crewaiCrewAI’s MCPServerHTTP connects to any HTTP-based MCP server and exposes its tools directly to agents via the mcps parameter.
from crewai import Agent, Task, Crew, Process
from crewai.mcp import MCPServerHTTP
MODEL = "openrouter/anthropic/claude-sonnet-4-6"
SELTZ_API_KEY = "your_seltz_api_key"
TARGET_COMPANIES = ["Datadog", "Cohere", "Weights & Biases"]The Seltz MCP server runs at https://mcp.seltz.ai/mcp. One connection object, passed directly to whichever agents need retrieval access.
seltz_mcp = MCPServerHTTP(
url="https://mcp.seltz.ai/mcp",
headers={"x-api-key": SELTZ_API_KEY},
cache_tools_list=True,
)Defining the agents
signal_hunter = Agent(
role="GTM Signal Hunter",
goal="Find recent trigger events at target companies",
backstory="""You search Seltz news scope to find leadership hires,
funding rounds, and expansion announcements at target companies.
You extract the person involved, their role, the event, and the date.
Always use scope='news' when searching.""",
mcps=[seltz_mcp],
llm=MODEL,
verbose=False,
)
people_enricher = Agent(
role="People Research Specialist",
goal="Retrieve full career records for identified contacts",
backstory="""Given a person's name and company, you query Seltz people scope
to retrieve their complete role history, prior companies, and education.
Always use scope='people' when searching for people.""",
mcps=[seltz_mcp],
llm=MODEL,
verbose=False,
)
outreach_strategist = Agent(
role="Outreach Strategist",
goal="Merge trigger events and profiles into a ranked outreach list",
backstory="""You synthesise research from the signal hunter and people enricher
into a ranked list of outreach opportunities, each with a specific first line
written around the trigger event.""",
llm=MODEL,
verbose=False,
)Note that the Outreach Strategist has no mcps. It reasons over what the first two agents return, not over additional retrieval.
Defining the tasks
signal_task = Task(
description=f"""Search the Seltz news scope for recent trigger events
at these companies: {TARGET_COMPANIES}.
For each company, look for leadership hires in data or AI functions,
funding rounds, product launches, or expansion announcements
published after 2025-01-01.
Use scope='news' and from_date='2025-01-01'.
Return: company name, person name, their role, the event, and why
this is a good moment to reach out.""",
agent=signal_hunter,
expected_output="Structured list of trigger events per company with person, role, event, and outreach rationale"
)
enrich_task = Task(
description="""For each person identified in the trigger events,
query the Seltz people scope to retrieve their full career record.
Search by person name and company. Use scope='people'.
Return: full role history with dates, prior companies, education,
and LinkedIn URL for each person.""",
agent=people_enricher,
expected_output="Full structured profiles for each identified contact",
context=[signal_task]
)
outreach_task = Task(
description="""Using the trigger events and enriched profiles,
build a ranked outreach list.
For each contact produce:
- Full name and current role
- Company
- The trigger event that makes now the right moment
- A specific first outreach line that references the trigger event
and their background
- A signal strength score from 1 to 10
Rank by signal strength, strongest first.""",
agent=outreach_strategist,
expected_output="Ranked outreach list with personalised first lines",
context=[signal_task, enrich_task]
)Running the crew
crew = Crew(
agents=[signal_hunter, people_enricher, outreach_strategist],
tasks=[signal_task, enrich_task, outreach_task],
process=Process.sequential,
verbose=True,
)
result = crew.kickoff()
print(result)We wrapped this pipeline in a Streamlit app to make it easier to run and watch the agents work in sequence.
The sidebar shows the three agents and which Seltz scope each one calls.
The main panel updates as each agent completes, showing trigger events found, profiles enriched, and outreach list ready. The ranked cards below show the final output.
Each outreach message is written around the specific trigger event and the person's background, context that only exists because both the people record and the news record were complete enough to read and join.
When to chain with open web search
Seltz handles two jobs in this pipeline well. It reliably returns full records at the director and regional-leader layer, and surfaces recent news with complete article text.
For the single most senior executive at a company, open web search is the stronger first call.
The reliable pattern is to pin the top name on the open web, then use Seltz to enrich the full layer below.
Similarly, for discovery queries where you don’t yet know which companies to target, open web search handles that job. Seltz earns its place once you have a list and need depth on each one.
A pipeline that chains both gets the best of each, using open web for discovery and Seltz for the depth that makes the join possible.
Brendan Short’s observation was about timing being arbitrary. When the retrieval layer returns complete records, the timing problem solves itself, and the signal finds you.
Thanks to Seltz for partnering today!
5 context compaction strategies for LLM agents
Compacting your agent’s context can cut its tokens and still raise costs.
This sounds counterintuitive, but token count and billed amount are two different quantities in how prefix caching works.
A long agent session resends its entire history on every call, appending the model’s reply and the tool output to the transcript each turn.
This stays affordable because the leading span of each request is byte-identical to the previous one.
Providers bill that span as a cache read, which on Anthropic is 10% of base input, so a context that grows only at the tail stays cheap no matter how large it gets.
Compaction edits the front of the transcript rather than the tail. The harness replaces the original turns with a summary, so everything from the edit point onward stops matching what was cached.
Consider a session having 100K tokens of history.
Normally, the next call reads that history from cache at a tenth of base rate, which works out to 10K tokens at full price.
But compact it down to a 10K summary, and there is nothing left to match, so those 10K bill as a cache write at 1.25x base input, which comes to 12.5K. The context is ten times smaller, and the call costs more.
To be fair, that cost is recovered over the turns that follow. But harnesses trigger compaction on a token threshold, so a long session compacts repeatedly and each event resets it.
None of this makes compaction wrong. Context windows are finite, and there are five strategies used in practice.
> Truncation drops the oldest tokens once the limit is close. It is the cheapest to implement and the only one that permanently loses early decisions.
> Rolling summarization merges each new summary into a persistent state instead of regenerating from scratch, and still moves the cache boundary every time.
> Prompt compression scores each token with a small model and drops the low-relevance ones. LLMLingua reports up to 20x compression at small accuracy loss, and LLMLingua-2 does the same scoring with a BERT-sized encoder.
> RAG-based retrieval moves the history into a vector DB and injects back only what matches the current query, so retrieval precision becomes the failure mode instead.
The first three delete text outright, and RAG moves it into a store the agent only sees again if retrieval fetches it.
> KV cache eviction runs at the serving layer and drops the entries least likely to be needed, either by attention score, as in H2O and SnapKV, or by position, as in StreamingLLM.
The full history still goes to the model, and what gets dropped is the KV tensors the GPU computed for those tokens. Since those tensors are derived from the tokens, eviction costs prefill work rather than information.
They can always be recomputed. KV blocks that no longer fit in GPU memory can move to CPU DRAM, local NVMe, or a remote store, then load back on the next request instead of being recomputed during prefill.
LMCache implements this as an open-source layer for vLLM, SGLang, and Dynamo. Through CacheBlend, it reuses cached blocks at any position in the prompt rather than only the leading span.
Repo: https://github.com/LMCache/LMCache
(don’t forget to star it ⭐)
Good day!












