Coding Agents for Historical Research

Bas Machielsen

What is a coding agent?

Accessing a model from Python

An application programming interface (API) lets a Python program send prompts to a model and receive its responses. chatlas handles this communication.

from chatlas import ChatAnthropic

chat = ChatAnthropic()
chat.chat("Explain what a company director does.")
chat.chat("How does that differ from a shareholder?")

Python program

Provider’s API

Model

Response

Each API call sends the model name and the full conversation so far to the provider’s server, which runs the model and returns text. The model keeps no memory between calls, so chatlas resends earlier messages; hence the second question can refer to “that”.

Giving the model a function

A tool is a function made available to the model. The model requests a call and supplies its arguments. The surrounding software executes the function and returns its result.

def multiply(x: int, y: int) -> int:
    """Multiply two integers exactly."""
    return x * y

chat.register_tool(multiply)
chat.chat("Use multiply to calculate 20598162 × 83106206.")

The model never sees the code. It receives a text description of the tool (its name, docstring, and argument types) with every request, and decides from the prompt and that description whether a call would help. The model cannot run the function itself: it replies with a structured request, and chatlas executes multiply in the Python session on the researcher’s own computer.

Anatomy of a tool call

One call to chat.chat() works as follows:

Step What happens
1. Prompt Use multiply to calculate 20598162 × 83106206.
2. Model reasons Exact eight-digit multiplication is error-prone; multiply is available.
3. Tool call The model replies multiply(x=20598162, y=83106206)
4. Execution Python, on your computer, returns 1711835094393372
5. Result chatlas adds the result to the conversation and calls the model again
6. Answer 20598162 × 83106206 = 1,711,835,094,393,372.

Steps 2 to 6 happen inside a single chat.chat() call. The provider is called twice: once to obtain the tool request, and once more, with the result added to the conversation, to obtain the answer.

Giving the model access to files

The same mechanism gives a model access to project files. Reading supplies information; writing creates a saved research output.

from pathlib import Path

def read_file(path: str) -> str:
    """Read a UTF-8 text file."""
    return Path(path).read_text(encoding="utf-8")

def write_file(path: str, content: str) -> str:
    """Save text to a UTF-8 file."""
    Path(path).write_text(content, encoding="utf-8")
    return f"Saved {path}"

chat.register_tool(read_file)
chat.register_tool(write_file)

From tool calls to an agent

chat.chat(
    "Read directors.csv. Summarize the directors, firms, "
    "and periods. Save the summary to director_summary.md."
)
  1. Model: read_file("directors.csv")
  2. Python returns the contents.
  3. Model: write_file(...)
  4. Python saves; model replies with text.
response = model(messages, tools)
while response.tool_calls:
    results = [run(c) for c in response.tool_calls]
    messages += [response, results]
    response = model(messages, tools)
print(response.text)

An agent is this loop: after every tool result the model is called again, until it replies with text instead of a tool request. The model decides the next action and when the task is finished.

Tools for coding and research

Coding agents supply tools for inspecting files, editing code, and running commands. Web tools let them find sources and inspect their contents.

Tool Example action
Read / list / search files Locate and inspect directors.csv
Write / edit files Create a Python retrieval script
Run a command Execute the script and inspect its output
Web search Find biographies or genealogy records
Web fetch / browser Open a source and examine its contents

Write Python

Run it

Inspect the result

Choose the next action

The agent’s working environment

The terminal and the project folder

Coding agents such as Claude Code and Codex run in a terminal: a window in which programs are started by typing commands. Every command runs from a current folder, the working directory. An agent started in a project folder treats that folder as its workspace.

The project folder

director_research/
├── directors.csv
├── sources/
│   └── biography.pdf
├── code/
└── outputs/

A terminal session inside it

$ cd ~/research/director_research
$ ls
code  directors.csv  outputs  sources
$ head -3 directors.csv
name,birth_year,firm,role
Frederik Hendrik Fentener van Vlissingen,1849,SHV,director
Gerard Philips,1858,Philips,director

cd enters the folder, ls lists it, and head prints the first lines of a file. A relative path such as sources/biography.pdf starts from the working directory. The agent’s file and command tools resolve paths in the same way.

The agent acts on your computer

Example prompt:

Read directors.csv. Write a Python script that counts directors by firm. Run it, inspect the result, and save the script and a summary under code/ and outputs/.

Actions the agent takes

Read   directors.csv
Write  code/count_by_firm.py
Bash   python code/count_by_firm.py
Read   (output of the script)
Write  outputs/firm_counts.md

Each action runs on the researcher’s own machine, with the researcher’s permissions. The same tools can install packages, overwrite files, or delete them. Claude Code and Codex therefore ask for approval before edits and commands unless they have been permitted in advance.

Work in a dedicated project folder under version control (git), so that every change the agent makes can be inspected and reverted.

What an agent reads directly

An agent works through its tools, and its file tools handle text. What a document requires before the model can use it depends on its format.

Plain text
.txt .md .csv .py .json
Read directly

Packaged or laid-out
.docx .pptx .pdf
Extract text first

Images of text
scans, photos, image-only PDFs
OCR first

  • Plain text is native: the read tool returns the contents as they are, searches them, and an edit changes specific lines.
  • DOCX and PPTX files are zipped packages of XML, and a PDF records where characters sit on a page; a program has to extract their text, and reading order, tables, and footnotes can break in the process.
  • Scanned pages contain no text at all, only images. Their text has to be recognised first (OCR), for example by a vision model, and the result checked against the scan.

Converting documents with anydoc

Markdown is plain text with light markup for headings, lists, links, and tables. It keeps a document’s structure while remaining native to the agent. The practical rule: convert once, work on the Markdown copy, keep the original.

anydoc converts PDF, DOCX, PPTX, and similar formats into Markdown, one file per command. It requires Node.js and is installed once:

npm install -g @firecrawl/anydoc
anydoc sources/biography.pdf -o sources/biography.md
anydoc sources/notes.docx -o sources/notes.md
anydoc sources/scan.pdf --ocr hosted -o sources/scan.md   # scanned PDF, usually better alternatives

An agent does not know that anydoc is installed unless it is told. State it in the prompt, or once in the project’s instruction file.

anydoc documentation (version 0.2.4).

Working with an agent

Launching an agent

Start the agent from the project directory. Claude Code and Codex provide interactive sessions in which an agent can inspect files, run commands, and save work.

Claude Code

cd director_research
claude

Codex

cd director_research
codex

Enter a task in the session. Read the response, inspect the files it creates, and continue with follow-up instructions.

Many other coding agents follow the same design, among them Gemini CLI, GitHub Copilot CLI, and Cursor. Open-source agents such as OpenCode, Aider, and Goose are free to use and work with most providers; they can also run a model locally through Ollama, in which case no data leaves the computer, at the cost of a less capable model.

Specifying the task and output

A task names three things: the input, the objective, and the output. Paths are given relative to the project folder.

Read directors.csv. Summarize the directors, firms, and periods represented in the file. Save the summary to outputs/director_summary.md. Report missing information explicitly and list the files created.

Element In this prompt
Input directors.csv
Objective A summary of directors, firms, and periods, with missing information reported
Output outputs/director_summary.md

A named output file makes the result available outside the conversation: the researcher inspects the file rather than relying on the agent’s description of it. File names can also be left to the agent, or fixed once for the whole project through naming conventions in AGENTS.md, as shown below.

Working interactively

The slowest way to work with an agent is also the most supervised: every response is read, and the next instruction corrects or extends it.

Initial instruction

Summarize the director list and save it to outputs/director_summary.md.

Follow-up

The period column describes service at the firm, not the lifespan. Make that distinction explicit.

Further instruction

Add a section on the information needed to distinguish directors with similar names.

This mode shows what the agent does and which instructions it lacks. Writing those instructions down lets later runs proceed with less supervision:

Interactive session

Persistent instructions

Saved prompts

Delegated research

Persistent project instructions

An AGENTS.md file in the project folder holds instructions that apply to every session. Claude Code, Codex, and most other coding agents read it automatically when they start.

AGENTS.md

# Director research
## Commands
Run scripts with Python. Convert documents with `anydoc`.
## Project layout
sources/ holds original documents; never edit them.
code/ holds scripts; outputs/ holds results.
Name files in lowercase snake_case: outputs/firm_counts.csv
## Code and writing style
Run scripts from the project root with relative paths.
Write short functions with docstrings; use pandas for tables.
Write notes in full sentences and state uncertainty.
## Evidence
Compare names, dates, places, and family.
Save source URLs, identity decisions, and unresolved cases.

The file typically records the commands available in the project, the project layout, naming conventions for new files, the style of code and notes, and the evidence standard.

With the layout and naming conventions in place, a prompt no longer has to name every output file: the agent places and names new files itself, consistently across sessions.

Corrections that had to be repeated in interactive sessions belong here.

Saving a prompt in a file

A task that recurs, such as summarizing a list each time it is updated, can be saved as a plain text file and reused. The following file contains the same instruction as before:

prompts/summarize.md

Read directors.csv. Summarize the directors, firms, and service periods.
Write outputs/director_summary.md. Report missing information and list the files created.

The prompt refers to the list in directors.csv. Its first rows are:

directors.csv (first rows)

name,birth_year,firm,role
Frederik Hendrik Fentener van Vlissingen,1849,SHV,director
Gerard Philips,1858,Philips,director
Samuel van den Bergh,1864,Van den Bergh,director
Ernst Heldring,1871,KNSM,director

The next slide runs this prompt file without opening an interactive session.

Running a saved prompt

Claude Code and Codex both have a non-interactive mode. The agent reads the prompt, carries out the task, writes its files, and exits.

Claude Code

claude -p < prompts/summarize.md > outputs/claude-summary.md

Here -p runs the agent without an interactive session, < supplies the contents of the prompt file, and > saves the agent’s final message to a file.

Codex

codex exec --sandbox workspace-write -o outputs/codex-summary.md - < prompts/summarize.md

The summary itself is written to outputs/director_summary.md, as the prompt requests. The command can be rerun whenever the list changes; permissions must be granted in advance, because no one is present to approve them.

A research workflow

The research task

The file directors.csv lists company directors. For each director, the aim is to identify the parents, spouse, and children, and then to establish with which firms those relatives were affiliated, in which role, and in which period. The result is a dataset with one row per relative and affiliation, each linked to its source.

The worked example is one director from the list: Frederik Hendrik Fentener van Vlissingen, born in 1849, director of SHV.

Four steps followed in this task

1. Find the director in the civil records

2. Retrieve the family

3. Research the relatives’ affiliations

4. Combine into a dataset

Steps 1 and 2 rely on Dutch civil records, Step 3 on web search.

Director and family details. Workflow adapted from the genealogical research in my papers.

Civil records and OpenArchieven

Since 1811, Dutch municipalities have registered births, marriages, and deaths in the civil registry (burgerlijke stand). Each certificate names several people in fixed roles:

Record People named on the certificate
Birth The child, the father, and the mother
Marriage Groom and bride, with ages and birthplaces, and the parents of both
Death The deceased, the parents, and the spouse

OpenArchieven indexes these certificates from Dutch regional archives and makes them searchable by name. It also offers an API: a web address that returns search results as structured data (JSON) instead of a web page, so that a Python script can query it.

An agent proceeds as a researcher would: search the director’s name, open his marriage record for parents and spouse, then find records naming him as father.

Web search and web fetch

An agent can search the web much as a researcher uses Google: it submits a query, reads the list of results, opens the pages that look relevant, and extracts names, dates, and roles from them. Two tools make this possible.

Web search takes a query and returns a list of results: titles, URLs, and short snippets. Results are leads.

Web fetch takes a URL, downloads the page, and returns it as text or Markdown for the model to read. Some sites block automated access.

Tool calls for the example director:

WebSearch "Fentener van Vlissingen" 1849 SHV
  → genealogieonline.nl, huygens.knaw.nl, ...
WebFetch  genealogieonline.nl/.../I11117.php
  → spouse, marriage date, children
WebFetch  resources.huygens.knaw.nl/.../fentener
  → the son's firms and roles

Search and fetch suit open-ended exploration, where the next query depends on what the last page said. Where a source offers an API, as OpenArchieven does, a Python script retrieves structured records more reliably. Step 1 does that.

Finding the director in the civil records

Step 1 of 4

The agent writes a short Python script that searches OpenArchieven for the director’s full name, runs it, and reads the results.

import requests

def search_records(name: str) -> dict:
    response = requests.get(
        "https://api.openarchieven.nl/1.1/"
        "records/search.json",
        params={"name": name, "number_show": 5},
        timeout=30,
    )
    return response.json()["response"]

result = search_records(
    "Frederik Hendrik Fentener van Vlissingen"
)

The search returns 71 person mentions, not 71 people. The son has the identical name (1882–1962), and the director also appears as father in the son’s death record.

The agent compares birth dates, roles, and spouses. The 1879 marriage record names a groom born on 21 September 1849, matching the data file; this record becomes the anchor for the family.

OpenArchieven API query, retrieved 30 September 2026; 1879 marriage record.

Retrieving the family

Step 2 of 4

Once the director is identified, the agent continues without further prompts. It reads the parents and spouse from the marriage record, searches for records that name the director as father, checks each candidate against names and dates, and then moves to the next director on the list. This is the agent loop from the first section, applied to archival research.

Read the roles recorded on the marriage certificate

people = {p["@pid"]: p for p in record["Person"]}
roles = {
    r["RelationType"]: people[r["PersonKeyRef"]]
    for r in record["RelationEP"]
}
father = roles["Vader van de bruidegom"]
mother = roles["Moeder van de bruidegom"]
spouse = roles["Bruid"]

Simon Paulus + Anna Maria Veldhuijs

F.H., born 1849 + Lijntje Roosenburg

F.H. (Frits), born 1882

Researching affiliations

Step 3 of 4

For each relative found in Step 2, the agent searches the web for firm affiliations. The task is written once as a template; for the director’s son:

Find the firms with which F.H. Fentener van Vlissingen (born 1882) was affiliated. Record each firm, role, and period with its source.

Searches: name variants with firms and roles

"Frits Fentener van Vlissingen" commissaris
"F.H. Fentener van Vlissingen" Hoogovens
"Frederik Hendrik Fentener van Vlissingen" KLM

Extracted from a biography of the son

Hoogovens: delegated supervisory director, 1918–1945; chairman of the supervisory board, 1945–1953.

New firms, such as KLM, become the next queries. The step repeats for every relative.

The resulting dataset

Step 4 of 4

Python combines the family links (Step 2) and affiliations (Step 3) into outputs/relatives.csv: one row per relative and affiliation, each with its source.

Relative Relation Born Firm Role Period
S.P. Fentener van Vlissingen Father not yet established
Anna Maria Veldhuijs Mother not yet established
Lijntje Roosenburg Spouse 1860 not yet established
F.H. Fentener van Vlissingen Son 1882 Hoogovens Delegated supervisory director 1918–45
F.H. Fentener van Vlissingen Son 1882 Hoogovens Chairman, supervisory board 1945–53

Open rows mark research still to be done. Identity decisions and open questions are saved in identities.md and unresolved.md.

Delegating research tasks

One task per director

The four steps of the previous section can be written down once, as a prompt file that takes a single row of directors.csv as its input. The agent that receives it chooses its own queries and follow-up actions.

prompts/research_director.md

You are given one row of directors.csv: name, birth year, and firm.
1. Find the director in OpenArchieven; distinguish namesakes by birth date and spouse.
2. Retrieve the parents, spouse, and children from the civil records.
3. Search the web for the firms, roles, and periods of each relative.
4. Save outputs/directors/<name-birthyear>/relatives.csv, one row per relative
   and affiliation with its source URL, and list open questions in unresolved.md.

The file is identical for every director on the list; only the row it receives changes. Each director’s results are saved in a separate folder.

One agent per director

A subagent is an agent started by the main agent, with its own conversation and tool calls. The main agent can start one for every row of the list.

Read directors.csv. For each row, spawn a subagent with that row and the instructions in prompts/research_director.md. Run them in parallel; each writes only to its own folder under outputs/directors/. Wait for all of them, then report which finished and which left questions open.

Main agent reads directors.csv

Fentener van Vlissingen, 1849

Philips, 1858

Van den Bergh, 1864

Heldring, 1871

Main agent collects the results

Reviewing and combining the results

Read each director’s relatives.csv and unresolved.md. Check that the identities and sources support the rows, and send a subagent back where evidence is missing. Use Python to combine the reviewed rows into outputs/relatives.csv and the open questions into outputs/unresolved.md.

outputs/
├── directors/
│   ├── fentener-van-vlissingen-1849/
│   ├── philips-1858/
│   ├── van-den-bergh-1864/
│   └── heldring-1871/
├── relatives.csv
└── unresolved.md

The combined file has the columns of the dataset from Step 4, with an added column naming the director. The main agent checks and combines; the researcher decides whether the evidence is sufficient for the analysis.

The workflow developed for one director thus extends to the whole list. Saved prompts, code, sources, and open questions keep each agent’s work inspectable.

The End