DEV Community

Cover image for How to Research Many Companies at Once with the DeepResearch Batch API
Prosper Otemuyiwa for Valyu AI

Posted on

How to Research Many Companies at Once with the DeepResearch Batch API

Day 29 of 30 Days of Search.

TL;DR

The Valyu DeepResearch Batch API runs the same research brief across many entities in parallel. You create a batch with the shared settings (mode, output format, sources), add one task per company (up to 100 per request), wait, then list every task's report, sources and cost.

We ran one brief across 10 payments companies: latest-year revenue, growth, net income and top risk factors from each 10-K, using only SEC filings. 10 of 10 tasks completed and 0 failed. The whole batch took 4.9 minutes from creation to the last report (about 24 minutes if run one after another) and cost $1.56 in total. Every revenue and net income figure matched SEC XBRL data.

The full script is about 100 lines of Python. The pattern that matters: save the batch ID, and reconcile every input to a result, rather than trusting a completed-only listing.

The job: one question, every company on the list

An analyst covering payments wants the same four facts for every name in the peer group: fiscal year, revenue and growth, net income, and the risks management stresses most. A VC reviewing a portfolio, a sales team qualifying accounts and a credit team refreshing a watchlist all have the same shape of problem.

Running one DeepResearch task at a time works for three companies. At ten or fifty, you end up writing your own queue, tracking IDs, retrying, and adding up costs. The Batch API does that bookkeeping:

  • Shared settings, set once. mode, output_formats and search live on the batch. Every task inherits them and cannot override them, so every company is researched the same way.
  • Per-task inputs. Each task has its own query and can set research_strategy, report_format, urls and metadata, plus your own id.
  • One place to track it. Batch status gives task counts (queued, running, completed, failed, cancelled) and the total cost.
  • One notification. Pass webhook_url to be called when the batch finishes instead of polling.

A batch holds the shared settings: fast mode, markdown output and SEC filings as the only source. Each company becomes its own task with an id, query, research strategy and report format. Tasks run in parallel, and each one is reconciled to a saved report or a recorded error.

Shared settings live on the batch. Only the company changes from task to task.

Step 1: Install and set your key

pip install valyu
export VALYU_API_KEY=your-key   # from platform.valyu.ai
Enter fullscreen mode Exit fullscreen mode

New accounts get free credits, no credit card required. Keep the key server-side.

Step 2: Write the brief once

The brief is three strings. The query names the company and the job; the strategy says where to look; the report format fixes the shape of the answer so ten reports read the same.

STRATEGY = (
    "Use the company's most recent annual report on Form 10-K. "
    "Read the income statement and Item 1A. Do not use analyst estimates."
)
FORMAT = (
    "Exactly these sections: Fiscal year; Revenue (with prior-year figure and % change); "
    "Net income; Three most emphasised risk factors (one line each); Sources. "
    "Cite the filing for every figure. Under 250 words."
)
Enter fullscreen mode Exit fullscreen mode

Restricting the batch to valyu/valyu-sec-filings keeps every figure traceable to a filing. Drop the restriction if you want web, news or other data sources in the mix.

Step 3: Create the batch and add one task per company

from valyu import Valyu

valyu = Valyu()  # reads VALYU_API_KEY

batch = valyu.batch.create(
    name="payments-10k-screen",
    mode="fast",
    output_formats=["markdown"],
    search={"included_sources": ["valyu/valyu-sec-filings"]},
)

added = valyu.batch.add_tasks(batch.batch_id, [
    {
        "id": key,  # your own ID, returned on every result
        "query": f"{name}: latest fiscal-year revenue, revenue growth, net income "
                 "and top three risk factors from its most recent 10-K.",
        "research_strategy": STRATEGY,
        "report_format": FORMAT,
    }
    for key, name in COMPANIES.items()
])
print(added.added, "tasks queued in", batch.batch_id)
Enter fullscreen mode Exit fullscreen mode

COMPANIES is a plain dict, {"visa": "Visa (V)", "mastercard": "Mastercard (MA)", ...}. Use your own short keys as task IDs; they come back on every result, which makes reconciliation trivial.

Two things worth knowing before you add tasks:

  • Billing is all-or-nothing per add request. Adding tasks holds the mode price of every task in that request. If your balance is too low you get a 402 and none are added. Creating the batch itself is free.
  • Pick the mode for the job. fast is 25 credits per task and is the mode the docs suggest for batches. standard is 100, heavy 500. Our brief is a narrow lookup, so fast fits.

Save batch.batch_id before you do anything else. If your process dies, the research keeps running and you collect it later with the ID.

Step 4: Wait, then reconcile every task

final = valyu.batch.wait_for_completion(
    batch_id,
    poll_interval=15,
    on_progress=lambda s: print(f"{s.batch.counts.completed}/{s.batch.counts.total} done"),
)

tasks, last_key = [], None
while True:  # results are paginated
    page = valyu.batch.list_tasks(batch_id, include_output=True, last_key=last_key)
    tasks += page.tasks or []
    if not page.pagination.has_more:
        break
    last_key = page.pagination.last_key

for task in tasks:
    if task.status == "completed":
        Path(f"reports/{task.task_id}.md").write_text(str(task.output))
    print(task.task_id, task.status, len(task.sources or []), task.cost, task.error or "")
Enter fullscreen mode Exit fullscreen mode

The loop lists every task, not just completed ones. A status="completed" filter silently drops failed and cancelled tasks, so a clean-looking results page is not proof the whole batch succeeded. The full script writes one row per company to reports/index.csv: status, source count, cost, report file and any error.

wait_for_completion returns on completed or completed_with_errors, and raises if the batch is cancelled or the wait times out. Failed tasks are not charged.

What happened when we ran it

We ran the brief across 10 US-listed payments companies in fast mode today.

Run Result
Tasks 10
Completed / failed 10 / 0
Time to the last report 4.9 min
Sum of individual task times 24.4 min
Total cost $1.56 ($0.156 per task)

All 10 tasks were running within seconds of being added. Run one after another, the same tasks would have taken about 24 minutes; in the batch, the slowest task set the pace.

Company Status Task time Sources cited What the report says (reviewed against its cited filing)
Visa completed 4.8 min 2 FY to 30 Sep 2025: revenue $40.0bn, +11%; net income $20.1bn, +2%
Mastercard completed 1.5 min 1 FY2025: revenue $32.8bn, +16%; net income $15.0bn, +16%
American Express completed 1.7 min 2 FY2025: revenue net of interest expense $72.2bn, +10%; net income $10.8bn, +7%
PayPal completed 3.1 min 1 FY2025: revenue $33.2bn, +4.3% (computed); net income $5.2bn
Block completed 2.4 min 2 FY2025: revenue $24.2bn, +0.3%; net income $1.3bn vs $2.9bn, and flags a ~$1.9bn tax benefit in 2024
Fiserv completed 1.8 min 2 FY2025: revenue $21.2bn, +3.6%; net income attributable $3.5bn
Global Payments completed 1.8 min 2 FY2025: revenue $7.7bn, -0.4%, continuing operations only; flags Issuer Solutions as discontinued
FIS completed 2.4 min 2 FY2025: revenue $10.7bn, +5%; net earnings attributable $382m vs $1,450m, with Worldpay recast as discontinued
Affirm completed 3.6 min 2 FY to 30 Jun 2026: revenue $4.3bn, +32.1% (computed); net income $1.9bn, mostly a $1.4bn tax benefit, which the report calls out
Toast completed 1.2 min 1 FY2025: revenue $6.2bn, +24%; net income $342m vs $19m

We checked every revenue and net income figure, current and prior year, against the companies' XBRL data on SEC EDGAR. All 40 numbers matched. The reports also did things we did not ask for: six said the filing does not rank risk factors and explained which ones they picked, and four flagged a comparability problem (a one-off tax benefit, discontinued operations or a recast prior year). Most tasks cited one or two filings because the batch was limited to SEC filings, which is what you want for this kind of brief. Note the fiscal years differ: Visa's ends in September and Affirm's in June, which a naive spreadsheet would silently mix.

Here is the Global Payments report as it came back. We only changed the formatting: headings became bold text, dashes became commas and the source list is cut:

Global Payments FY2025: Revenue, Net Income and Leading Risk Factors

Fiscal year
FY2025, ended December 31, 2025; Form 10-K filed February 20, 2026 [1].

Revenue
$7,705.9 million, down $30.1 million, or 0.4%, from $7,736.0 million in FY2024 [2]. Figures cover continuing operations (Merchant Solutions); Issuer Solutions is reported as a discontinued operation, so FY2025 revenue is not comparable to pre-2025 total-company revenue [1].

Net income
$1,456.0 million, versus $1,644.2 million in FY2024 [1]. Net income attributable to Global Payments was $1,400.1 million, after $55.9 million attributable to noncontrolling interests [1].

Three most emphasised risk factors

  • Inability to protect systems and data from evolving cybersecurity threats [1].
  • Software and hardware defects, failures, undetected errors and development delays [1].
  • Dependence on third-party relationships and key supplier agreements [1].

These are the first three bullets of the Item 1A summary; the filing does not rank risks [1].

Batch or Individual tasks?

Batches trade flexibility for consistency. Every task inherits the batch's mode, output formats and search settings, and some features are not available in a batch at all.

A comparison of when to use the Batch API, for the same brief across many entities with shared settings, against individual DeepResearch tasks, for deliverables, attached files, MCP servers, human review checkpoints or a different mode per task.

Batch tasks inherit mode, output formats and search settings. Anything task-specific belongs in an individual task.

You need Use
The same brief across 10 to 100+ entities Batch
One total cost and one completion webhook Batch
CSV, Excel, PowerPoint or Word deliverables Individual task
Your own files, MCP servers or previous reports Individual task
Human review checkpoints before research continues Individual task
A heavier mode for one company in the list A separate batch, or an individual task

Code execution is always on in batch tasks.

Production notes

  • Use a webhook instead of polling. Pass webhook_url to batch.create. The webhook_secret is returned only on creation, so store it immediately and verify signatures.
  • More than 100 tasks? Call add_tasks again on the same batch while it is open or processing.
  • Keep your inputs. Store each submitted query with its task ID so you can match every input to a completed, failed or cancelled result.
  • Check the figures. A cited report is inspectable, not audited. Spot-check numbers against the cited filing, especially fiscal-year ends and restated figures, before they go into a model or a memo.

Frequently asked questions

How many tasks can a DeepResearch batch hold?

Each add_tasks request accepts 1 to 100 tasks, and you can add more requests to the same batch while it is open or processing. Actual concurrency depends on your account limits and capacity.

How much does a batch cost?

Each task costs its mode price: 25 credits in fast, 100 in standard, 500 in heavy, 2,500 in max. Creating a batch is free, failed tasks are not charged, and the batch cost field is the sum of its tasks. See Valyu pricing.

Can each task in a batch use different settings?

Partly. Each task can set its own query, research_strategy, report_format, urls and metadata. mode, output_formats and search come from the batch and cannot be overridden per task.

Can a batch return structured JSON instead of markdown?

The batch's output_formats accepts a JSON Schema in place of markdown, applied to every task. You cannot mix a schema with markdown or pdf. We used markdown here; test a schema on a few tasks before scaling.

Is the Batch API different from running DeepResearch in a loop?

The research is the same. The difference is bookkeeping: shared settings, one status endpoint with counts and total cost, pagination over results, cancel for the whole batch and one webhook at the end.

Can I use TypeScript instead of Python?

Yes. The valyu-js SDK has the same methods (valyu.batch.create, addTasks, listTasks). See the TypeScript batch reference.

Run your own list

Pick a list you already maintain by hand: a peer group, a portfolio, a watchlist, a set of target accounts. Write the brief once, run it in fast mode on five names, read the reports, then scale up.

The complete script, with start and collect commands, is below.

"""Run one DeepResearch brief across a list of companies with the Valyu Batch API."""

import csv
import sys
from pathlib import Path

from valyu import Valyu

COMPANIES = {
    "visa": "Visa (V)",
    "mastercard": "Mastercard (MA)",
    "amex": "American Express (AXP)",
    "paypal": "PayPal (PYPL)",
    "block": "Block (XYZ)",
    "fiserv": "Fiserv (FISV)",
    "global-payments": "Global Payments (GPN)",
    "fis": "Fidelity National Information Services (FIS)",
    "affirm": "Affirm (AFRM)",
    "toast": "Toast (TOST)",
}

STRATEGY = (
    "Use the company's most recent annual report on Form 10-K. "
    "Read the income statement and Item 1A. Do not use analyst estimates."
)
FORMAT = (
    "Exactly these sections: Fiscal year; Revenue (with prior-year figure and % change); "
    "Net income; Three most emphasised risk factors (one line each); Sources. "
    "Cite the filing for every figure. Under 250 words."
)

OUT = Path("reports")


def start(valyu: Valyu) -> str:
    batch = valyu.batch.create(
        name="payments-10k-screen",
        mode="fast",
        output_formats=["markdown"],
        search={"included_sources": ["valyu/valyu-sec-filings"]},
    )
    if not batch.success:
        sys.exit(batch.error)

    added = valyu.batch.add_tasks(batch.batch_id, [
        {
            "id": key,
            "query": f"{name}: latest fiscal-year revenue, revenue growth, net income "
                     "and top three risk factors from its most recent 10-K.",
            "research_strategy": STRATEGY,
            "report_format": FORMAT,
        }
        for key, name in COMPANIES.items()
    ])
    if not added.success:
        sys.exit(added.error)

    print(f"{added.added} tasks queued in {batch.batch_id}")
    return batch.batch_id


def all_tasks(valyu: Valyu, batch_id: str) -> list:
    tasks, last_key = [], None
    while True:
        page = valyu.batch.list_tasks(batch_id, include_output=True, last_key=last_key)
        if not page.success:
            sys.exit(page.error)
        tasks += page.tasks or []
        if not page.pagination or not page.pagination.has_more:
            return tasks
        last_key = page.pagination.last_key


def collect(valyu: Valyu, batch_id: str) -> None:
    final = valyu.batch.wait_for_completion(
        batch_id,
        poll_interval=15,
        on_progress=lambda s: print(f"{s.batch.counts.completed}/{s.batch.counts.total} done"),
    )

    OUT.mkdir(exist_ok=True)
    with open(OUT / "index.csv", "w", newline="") as f:
        rows = csv.writer(f)
        rows.writerow(["company", "status", "sources", "cost_usd", "report", "error"])
        for task in all_tasks(valyu, batch_id):
            report = ""
            if task.status == "completed":
                report = f"{task.task_id}.md"
                (OUT / report).write_text(str(task.output))
            rows.writerow([task.task_id, task.status, len(task.sources or []),
                           task.cost, report, task.error or ""])

    counts = final.batch.counts
    print(f"{counts.completed} completed, {counts.failed} failed, ${final.batch.cost:.2f} total")


if __name__ == "__main__":
    client = Valyu()  # reads VALYU_API_KEY
    if sys.argv[1:2] == ["start"]:
        start(client)
    elif sys.argv[1:2] == ["collect"] and len(sys.argv) == 3:
        collect(client, sys.argv[2])
    else:
        sys.exit("usage: python screen.py start | collect <batch_id>")
Enter fullscreen mode Exit fullscreen mode

Get a key at platform.valyu.ai and read the Batch API guide for statuses, pagination and webhooks.

What list would you run first?

Top comments (0)