M

How to Build an Agent Graph That Finds Business Opportunities While You Sleep.

14 min readView source ↗

Cover image

Most opportunity research dies of exhaustion.

You decide to look into something, a market, a niche, a category of deal. You spend four hours on the first candidate. You pull the numbers, run the math, check the constraints, and conclude it is marginal. Then you look at the eleven other candidates on your list and quietly stop. Not because the method was wrong, but because four hours times twelve is a week of work for a handful of verdicts, and you have a job.

The people who find good opportunities are rarely smarter analysts. They just get through more candidates. Volume is the whole edge, and volume is exactly what a manual research process cannot produce.

This article builds a system that produces volume. Not a single agent grinding through a loop, but a small graph of specialized agents: one that scans, one that analyzes, one that verifies, and a routing rule that only wakes you for the candidates worth your attention. It runs overnight. In the morning you read a short list of verdicts instead of a long list of things you should probably look into.

One warning before the build, because it shapes everything below: most tasks do not need a graph. A single well-scoped agent with a good verifier is a loop, and reaching for a graph when a loop would do turns a two-hour task into a two-day framework project. This particular task earns its graph, and the section after next explains exactly why, so you can apply the same test to whatever you build next.

Why This Task Earns a Graph

A loop is just a single-node graph with an edge back to itself. Everything you know about loop design, the discover-plan-execute-verify cycle, the stop condition, the verifier, is the inside of one node. A graph is what you get when several loops need to hand off to each other, and graph engineering is the discipline of deciding how they connect.

So the question is never "graph or loop." It is "does this work split into distinct specialties that hand off." For opportunity scanning, it does, on four counts.

The work splits into genuine specialties. Finding candidates is a breadth task: search wide, filter loosely, produce a list. Analyzing a candidate is a depth task: pull specific numbers, run a formula, produce a verdict. Verifying an analysis is a skeptical task: check the inputs, hunt for the disqualifying detail, try to kill the deal. Those are three different jobs with three different success criteria, and cramming them into one agent's loop means the agent that found a promising candidate is also the agent deciding whether it is promising.

It needs fan-out and fan-in. One scan produces a dozen candidates. Analyzing them sequentially wastes the parallelism that is available for free. Fan the analysis out across candidates, then join the results back into one ranked list.

Failure needs isolating. One candidate with broken or missing data should fail on its own and get flagged, not corrupt the whole run or poison the ranking of everything else.

Verification must be independent. This is the decisive one. An agent that produced an analysis is the worst possible judge of that analysis, because it shares every assumption it just made. The single highest-value node in almost any agent graph is a separate, read-only verifier whose only job is checking another node's work.

Four triggers, all firing. That is what justifies the structure. If your task only fires one of them, build the loop instead.

The Architecture

The graph has four nodes, five edges, and one shared state object that travels along them.

┌─────────────────┐

 schedule ─────►│    SCANNER      │

                │  finds candidates│

                └────────┬────────┘

                         │

                state: {criteria, candidates[]}

                         │

                    ┌────┴────┐  fan-out

                    ▼    ▼    ▼

                ┌─────────────────┐

                │    ANALYZER     │   (one per candidate,

                │  scores + math  │    running in parallel)

                └────────┬────────┘

                    └────┬────┘  fan-in

                         │

          state: {..., analyses[] with verdicts}

                         │

                         ▼

                ┌─────────────────┐

                │    VERIFIER     │

                │ tries to kill it│

                └────────┬────────┘

                         │

          state: {..., confirmed[], rejected[]}

                         │

                verdict == STRONG?

                         │

             ┌───────────┴───────────┐

                     yes                                   no

                       │                                       │

                      ▼                                      ▼

      ┌─────────────┐         ┌──────────────┐

      │               BRIEF     │         │   ARCHIVE    │

                │ wakes you   │         │ silent log   │

      └─────────────┘         └──────────────┘

Four nodes. The conditional edge at the bottom is the one that makes the system livable, because it is the difference between a report you read and a report you ignore. Everything that is not STRONG goes to an archive you can consult but never have to read.

The Shared State

State is what turns a pile of agents into a system rather than a group chat that forgets everything. Define it explicitly before writing a single node, because state drift is the most common way graphs rot.

STATE OBJECT (travels along every edge):
{
  "run_id": "...",
  "run_date": "...",
  "criteria": {
    "domain": "what kind of opportunity",
    "hard_filters": ["non-negotiable constraints"],
    "thresholds": {"strong": "...", "marginal": "...", "skip": "..."}
  },
  "candidates": [
    {"id": "...", "name": "...", "raw_data": {...}, "source": "..."}
  ],
  "analyses": [
    {"candidate_id": "...", "numbers": {...}, "score": 0.0,
     "verdict": "STRONG | MARGINAL | SKIP", "reasoning": "...",
     "assumptions": ["what this verdict depends on"]}
  ],
  "verifications": [
    {"candidate_id": "...", "status": "CONFIRMED | REJECTED | UNCERTAIN",
     "failed_checks": [...], "reason": "..."}
  ],
  "errors": [
    {"candidate_id": "...", "node": "...", "error": "..."}
  ]
}

Two design decisions in there are worth calling out.

The assumptions field on every analysis exists so the verifier has something specific to attack. An analysis that does not state what it is assuming cannot be meaningfully checked, because the verifier has to reconstruct the reasoning before it can test it.

The errors array is what isolates failure. A candidate that breaks during analysis writes an error and drops out. It does not halt the run, and it does not silently disappear either, which is the failure mode where you never learn that a third of your candidates failed to process.

Node 1: The Scanner

The scanner's only job is breadth. It finds candidates and applies the hard filters that disqualify something outright. It does not evaluate quality, because evaluating quality is the analyzer's job, and a scanner that starts scoring things will quietly narrow the funnel using criteria nobody wrote down.

You are an opportunity scanner. Your only job is finding candidates 
and applying hard filters. You do not evaluate quality or rank 
anything — a separate analyzer does that.

DOMAIN: [what kind of opportunity you are scanning for]

HARD FILTERS (a candidate failing any of these is excluded):
- [non-negotiable constraint 1]
- [non-negotiable constraint 2]
- [legal or regulatory constraint, if any]

For each candidate that passes the hard filters, return:
{
  "name": "...",
  "source": "where you found it",
  "raw_data": { [the specific data points the analyzer will need] },
  "data_gaps": ["anything the analyzer will need that you could not find"]
}

Rules:
- Return every candidate that passes the filters, including 
  ones that look unpromising. Ranking is not your job.
- Never estimate a number you could not find. Put it in 
  data_gaps instead. A missing number is information; 
  an invented one is a corrupted verdict downstream.
- If a filter status is unclear rather than clearly passing, 
  include the candidate and flag the filter as UNCLEAR.

Return between 10 and 30 candidates. If fewer than 10 pass, 
say so rather than loosening the filters.

The instruction against estimating missing numbers is the most important line in the node. A scanner that fills a gap with a plausible-looking figure hands the analyzer a number that gets multiplied through a formula into a verdict, and nothing downstream can tell that the verdict was built on a guess.

Node 2: The Analyzer

The analyzer runs once per candidate, in parallel. It takes raw data and produces a verdict with explicit math and stated assumptions.


You are an opportunity analyst. You receive one candidate and 
produce a scored verdict. Show all calculations.

CANDIDATE: [raw_data from state]

SCORING FORMULA:
[your explicit formula, with the weights spelled out]

THRESHOLDS:
- STRONG: [specific numeric bar]
- MARGINAL: [range]
- SKIP: [below]

Produce:
1. NUMBERS: every input you used and where it came from
2. CALCULATION: the formula applied, step by step, shown fully
3. SCORE: the resulting number
4. VERDICT: STRONG / MARGINAL / SKIP
5. ASSUMPTIONS: every assumption this verdict depends on. 
   Be exhaustive. If you assumed a rate, a timeline, a 
   condition holding, or a number staying stable, list it.
6. RED FLAGS: anything that would change the verdict if true

Rules:
- If a required input is missing from raw_data, output 
  verdict INSUFFICIENT_DATA and name what is missing. 
  Do not estimate it.
- Show the arithmetic. A verdict without visible math 
  cannot be verified.
- Assumptions are not a formality. The verifier attacks 
  this list. An analysis with no stated assumptions is 
  an analysis that will be rejected.

The verdict thresholds are numeric on purpose. STRONG, MARGINAL, and SKIP as bands with specific numeric bars mean the routing edge later is a deterministic check rather than a model judgment call, which is what keeps the system from waking you for things that merely felt promising.

Node 3: The Verifier

This is the node that makes the whole graph trustworthy, and it is the one most people skip.

The verifier is a different agent from the analyzer, ideally running a different model, with one instruction that inverts its incentive: its job is to kill the deal, not confirm it. An agent asked to "check this analysis" tends to confirm it. An agent asked to find the reason this fails behaves completely differently.

You are a skeptical verifier. You did not produce this analysis. 
Your job is to find the reason this opportunity does not work.

You are not here to confirm. A verifier that approves everything 
provides no value. Assume something is wrong and find it.

ANALYSIS TO VERIFY: [the analysis object from state]

Run these checks and report each one separately:

1. ARITHMETIC: Recompute the calculation independently. 
   Does it produce the same result?
2. INPUTS: Is each number sourced, or was any of it estimated 
   or inferred? Flag every unsourced figure.
3. ASSUMPTIONS: Take each stated assumption and ask what 
   happens if it is false. Which single assumption, if wrong, 
   flips the verdict?
4. OMISSIONS: What cost, constraint, risk, or requirement 
   is missing from this analysis entirely?
5. CONSTRAINTS: Does this actually satisfy every hard filter, 
   or was one marked UNCLEAR and then treated as passing?

Then return:
{
  "status": "CONFIRMED | REJECTED | UNCERTAIN",
  "failed_checks": ["which of the five checks failed"],
  "reason": "the specific finding, not a general concern",
  "fatal_assumption": "the one assumption that would flip this"
}

Rules:
- CONFIRMED means all five checks passed. Not "looks reasonable."
- UNCERTAIN is a legitimate answer and is better than a 
  confident wrong one. Use it when a check cannot be resolved 
  with the information available.
- A general concern is not a finding. "Market conditions could 
  change" is noise. "Occupancy figure is a 3-month average in 
  a market with 60% winter decline" is a finding.

The distinction between a concern and a finding in that last rule is what separates a verifier that adds signal from one that adds anxiety. Vague hedging on every candidate is functionally the same as approving everything, because it gives you nothing to act on.

Node 4: The Router and the Brief

The routing edge is deterministic code, not a model decision. This matters: a prompt asking a model to "surface the good ones" reintroduces exactly the judgment the thresholds were designed to remove.

ROUTING RULE (checked in order, first match wins):

{ "if": "analysis.verdict == INSUFFICIENT_DATA",
  "then": "archive", "tag": "data_gap" }

{ "if": "verification.status == REJECTED",
  "then": "archive", "tag": "failed_verification" }

{ "if": "verification.status == UNCERTAIN",
  "then": "brief", "section": "needs_your_judgment" }

{ "if": "analysis.verdict == STRONG 
         AND verification.status == CONFIRMED",
  "then": "brief", "section": "act_on_these" }

{ "default": "archive", "tag": "below_threshold" }

Note that UNCERTAIN routes to the brief, not the archive. Those are the candidates where the system correctly declined to decide, and they are often the most interesting ones, because they are where a human judgment call actually adds value rather than duplicating what the machine already did.

The brief itself should be short enough to read with coffee:

Generate the morning brief from the confirmed results.

ACT ON THESE: [candidates that scored STRONG and passed 
verification]. For each: the name, the number that makes it 
strong, the one assumption it depends on most, and the 
single next action.

NEEDS YOUR JUDGMENT: [UNCERTAIN candidates]. For each: 
what the verifier could not resolve and what would 
resolve it.

RUN SUMMARY: candidates scanned, analyzed, rejected and why, 
errors encountered.

Under 400 words. If nothing scored STRONG, say that plainly 
in one line. Do not pad a thin run to look productive.

That last instruction prevents the most corrosive failure mode of any automated brief. A system that manufactures something interesting on quiet mornings teaches you to stop trusting it, and a brief you have learned to skim is worse than no brief.

Stop Conditions and Cost

A graph is many loops, which means a weak verifier or a missing bound now burns tokens in parallel rather than sequentially. Cap it explicitly, in the router's logic rather than in a prompt.

Set a maximum candidates per run, so a broad scan cannot fan out into hundreds of parallel analyses. Set a maximum retries per candidate, typically one, after which the candidate writes an error and drops out rather than looping. And set a hard per-run cost ceiling that halts the batch regardless of state when hit.

Then scope before you scale. Run the graph on five candidates first, measure what it actually costs and how good the verdicts are, and only then point it at thirty. This is the single discipline that separates people who run agent graphs comfortably from people who discover the cost after the invoice arrives.

Test It Before You Trust It

Four deliberate tests, run before you let this operate unattended.

Feed it a known bad candidate. Take an opportunity you already know does not work and confirm the verifier rejects it. If a deliberately broken candidate reaches your morning brief, the verifier is decorative.

Feed it a candidate with missing data. Confirm the scanner flags the gap rather than estimating, and the analyzer returns INSUFFICIENT_DATA rather than computing a verdict from an invented number.

Break one candidate mid-run. Confirm it writes an error, drops out, and appears in the run summary, without corrupting the ranking or halting the batch.

Run the worst case. Maximum candidates, maximum retries, longest analyses. Calculate the cost. If the number would bother you on a real invoice, your bounds are too loose.

Most systems that fail in production would have failed one of these immediately, if anyone had run it.

Build the Loop First

The full graph is four nodes, a state object, a routing rule, and a set of bounds. Do not build it all at once.

Start with a single loop: one agent that takes one candidate, analyzes it, and produces a verdict with stated assumptions. Run it by hand on a candidate you already understand and compare its verdict to your own. Tune the formula and thresholds until they agree with your judgment.

Then add the verifier as a genuinely separate step and feed it a candidate you know is bad. If it catches it, you have the only node that makes the rest safe to automate.

Only then add the scanner for breadth, the fan-out for parallelism, the routing rule, and the schedule.

That order matters, because it lets you verify each node independently before trusting them as a system. A graph assembled all at once and never tested role by role is exactly the kind of thing that runs beautifully overnight and hands you a confident, well-formatted, completely wrong list in the morning.

The edge was never being a better analyst. It was getting through more candidates without lowering the bar on any of them.

***Build the loop this week. Add the verifier next.

The graph is what you get once both of those work.***

Related articles