SEC EDGAR: record once, replay forever ¶
The
examples/edgar/
package scrapes SEC EDGAR — a real regulatory site with no API contract — into
typed domain objects. It is the flagship demonstration of what Conatus is for:
an agent authors a typed, by-reference workflow once, and Conatus compiles
that single run into plain, standalone Python that re-runs deterministically,
with no model in the loop.
Two things make it interesting. First, the pipeline speaks typed objects, not
JSON: actions return a non-serializable HTML document and non-serializable
list[Filing] records, and the agent moves them between steps by reference.
Second, the run is compiled into a recipe — real Python code written by
Recipe.to_code — so every
execution after the first is free, repeatable, and auditable.
The building blocks ¶
edgar.py defines the typed actions and the Filing record. Note that the
actions return domain objects, not strings:
@dataclass(frozen=True)
class Filing:
"""One row of a company's EDGAR filing list — an audit-ready record."""
form_type: str
description: str
filing_date: str
index_url: str
@action
def fetch_filings_page(cik: str, form_type: str = "10-K") -> HTML:
"""Fetch a company's EDGAR filing list as an HTML document."""
return filings_page(cik, form_type)
@action
def extract_filings(page: HTML) -> list[Filing]:
"""Extract the filing rows from a company's EDGAR filing-list page."""
return parse_filings(page)
list_filings.py (Task A) hands those actions to a task. The docstring is the
standard operating procedure the agent follows; the body stays empty:
@task(
actions=[fetch_filings_page, extract_filings],
config={"preferred_model": "openai:gpt-5.5", "max_turns": 6},
)
def recent_filings(cik: str, form_type: str) -> list[Filing]:
"""List a company's recent SEC EDGAR filings.
Recipe:
1. Call `fetch_filings_page(cik, form_type)` to get the filing-list page
(an HTML object). Pass it on by reference.
2. Call `extract_filings(page)` on that HTML to get the typed `Filing`
records.
3. Terminate, returning those filings.
"""
resolve_document.py (Task B) drills into one filing's index page and resolves
its primary document by node, and pipeline.py composes Task A and Task B
with ordinary Python — no orchestration layer:
filings = recent_filings(cik, "10-K") # Task A (agent)
latest = filings[0] # plain Python — pick the most recent
docs = primary_document(latest.index_url) # Task B (agent)
The centerpiece: replay with zero LLM calls ¶
example_recipe.py is a real Recipe.to_code output checked into the repo —
the compiled, model-free version of Task B. replay_demo.py imports it and runs
it against a live EDGAR page without loading any model:
from examples.edgar.example_recipe import primary_document as replay_recipe
result = replay_recipe(LATEST_10K_INDEX) # plain HTTP + parse, no tokens
The --live flag re-authors the recipe end to end instead: it runs Task B (the
one and only LLM spend), finds the freshly written
runs/<timestamp>/out/recipe.py, and executes that generated function
standalone.
Run it ¶
# Free, no API key: replay the checked-in recipe (the default path)
uv run python examples/edgar/replay_demo.py
# The agent-authored tasks (need OPENAI_API_KEY, cost a few cents)
uv run python examples/edgar/list_filings.py
uv run python examples/edgar/resolve_document.py
uv run python examples/edgar/pipeline.py
# Full record -> replay loop (about $0.30, opt-in)
uv run python examples/edgar/replay_demo.py --live
Source:
examples/edgar/