Skip to content

Reports

chronicle.reports renders an experiment's hypothesis, takeaways, or research document to a compiled PDF (via the chronicle-tex service) and hands you the bytes.

Reach it two ways: the stateless chronicle.reports API (every call takes the experiment id + kind), or the per-experiment sugar exp.reports.<kind> that binds both for you. Prefer the sugar.

Report kinds

exp.reports exposes one accessor per kind:

Accessor Kind For
exp.reports.hypothesis hypothesis Pre-experiment hypothesis document
exp.reports.takeaways takeaways Post-experiment analysis
exp.reports.research research General research writing (lit review, synthesis)

Render

Two modes, selected by the experiment's report_settings.<kind>.mode:

  • Template (default) — pass a structured payload; the server fills a .tex template.
  • Freeform — pass a raw tex_body; opt in per kind via update_settings first.
exp = chronicle.experiments.create(hypothesis_summary="...", config_yaml="...")

# Template mode (default) — sugar form.
report = exp.reports.hypothesis.render(payload={
    "title": "Ripple study",
    "abstract": "...",
    "hypothesis": "...",
    "plan": ["sweep coefficient", "measure convergence"],
})

# Equivalent stateless form.
report = chronicle.reports.render(exp.id, "hypothesis", payload={"title": "Ripple study", ...})

# Freeform mode — raw .tex instead of a payload.
report = exp.reports.takeaways.render(tex_body=open("draft.tex").read())

Pass template_asset_id to render against a specific template asset instead of the default. render returns a Report handle.

Compile status

render produces an asset whose compile_status is one of:

Status Terminal? Meaning
pending no Render accepted; compile not finished
compiled yes PDF is available
failed yes Compile failed — see compile_log
stale yes Inputs changed since this compile

A missing field reads as unknown. (Authoritative list: reference/client.md.)

Compile is async, but usually fast

On most deployments the render endpoint compiles synchronously, so compile_status is already compiled when render returns. Where the compile worker runs out-of-band, poll with wait_for_compile.

report.wait_for_compile(timeout=60.0, poll_interval=1.0)  # polls refresh() until terminal
if report.compile_status == "compiled":
    report.download_pdf("hypothesis.pdf")

wait_for_compile returns self once compile_status is terminal or timeout elapses — it does not raise on timeout, so always re-check .compile_status.

The Report handle

A Report wraps the asset metadata plus, once content is fetched, the compile body.

Properties: id, asset_type, name, asset_config, compile_status, mode (template/freeform), template_asset_id (or None), compile_request_id (or None), compile_log (Tectonic stdout/stderr — None until content is fetched).

Methods (mutators return self for chaining):

  • refresh() — re-fetch asset + content. report.refresh().compile_status.
  • wait_for_compile(timeout=60.0, poll_interval=1.0) — poll until terminal.
  • download_pdf(path=None) — return the PDF bytes; write to path if given. Raises RuntimeError if compile_status isn't compiled or no PDF exists.
  • download_source() — return the rendered .tex source as a string.

compile_log and PDF/source content load on the first download_pdf / download_source / refresh, or eagerly via chronicle.reports.get(asset_id).

Settings

update_settings replaces the experiment's report_settings — that's where you opt a kind into freeform mode:

chronicle.reports.update_settings(exp.id, settings={
    "hypothesis": {"mode": "freeform", "freeform_prompt": "Write a one-page hypothesis focusing on..."},
})

Frozen at commit

report_settings is frozen once the experiment is committed — update_settings returns 409 Conflict after that.

Fetch an existing report

report = chronicle.reports.get(asset_id)   # metadata + content in one call
pdf = report.download_pdf()