Skip to content

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/