# Share Great Expectations Data Docs — Without Standing Up a Bucket

Canonical: https://commareports.com/share-great-expectations-data-docs
Published: 2026-08-21

> Great Expectations builds Data Docs as a static site you then have to host. Publish a validation run to Comma with one curl instead: a stable URL, comments anchored to the failed expectation, and a revision per run.

# Share Great Expectations Data Docs

Great Expectations gives you the right artifact and the wrong distribution.
`build_data_docs()` renders a clean static site — suites, validation
results, which expectation failed on which column, with sample unexpected
values. Then it hands you a folder and the hosting problem.

The standard answer is a bucket. That answer comes with a bucket policy, a
decision about whether validation results containing sample row values
should be world-readable (they should not), a signed-URL or CDN layer to
avoid that, and a cleanup job for the runs nobody will read again. For a
team that just wants the analyst to see why last night's load failed, it's
a lot of infrastructure between the failure and the person who understands
it.

## The pattern

Run the checkpoint, then publish the run's HTML:

```python
import pathlib, requests, os

result = context.run_checkpoint(checkpoint_name="nightly_orders")
context.build_data_docs()

html = pathlib.Path(
    "great_expectations/uncommitted/data_docs/local_site/index.html"
).read_text()

requests.patch(
    f"https://commareports.com/api/v1/reports/{os.environ['REPORT_ID']}",
    headers={"Authorization": f"Bearer {os.environ['COMMA_API_TOKEN']}"},
    json={"title": f"Orders data quality — {result.run_id.run_time:%Y-%m-%d}",
          "html": html},
    timeout=30,
).raise_for_status()
```

Create the report once, keep its id in your secret store next to the
[scoped token](/docs/api-tokens) (`reports:write` is enough), and `PATCH`
it every run. One URL per suite; a revision per run; any two runs
diffable.

## What renders, honestly

Comma stores report HTML verbatim and renders it inside an iframe with
`sandbox="allow-scripts"` and no `allow-same-origin`, so a report's own
JavaScript runs. The constraint that bites here is different: GX's
generated site is a **multi-file, multi-page** artifact — collapsible
sections, inter-suite navigation, and the bundled CSS and JS all live in
sibling files, and a revision holds one HTML document.

Upload those sibling files as [assets](/docs/api) alongside the HTML and
relative `src`/`href` references are rewritten to the uploaded copies. But
for a per-run validation report, a **single self-contained page** is the
better artifact — it's what a reviewer reads, and it diffs cleanly:

- **Build a digest from the validation result JSON**, not from the
  generated site. `result.list_validation_results()` gives you everything
  a reviewer needs: suite name, batch identifier, each expectation, its
  status, observed value, and unexpected-value samples. A short template
  turns that into one HTML table.
- **Inline the CSS you need**, or upload `static/` as assets. A relative
  link to `static/styles/data_docs_custom_styles.css` resolves only if that
  file was uploaded with the report.
- **Attach the full site.** Zip `local_site/` and add it as an
  [asset](/docs/api) — 25 MB per file, 250 MB per report — for anyone who
  wants to click through the original.
- **Watch the row samples.** Unexpected-value samples are real customer
  data. That is an argument for a private or team-visible report, and
  against a public bucket — see the [sharing model](/docs/sharing).

## The part a static site can't do

A failed expectation is the start of a conversation, not the end of one.
`expect_column_values_to_not_be_null` failing on `customer_id` for 0.4% of
rows is either a pipeline bug or a known upstream quirk, and the person who
knows which one is usually not the person who read the report.

On a published run, the analyst highlights that row and pins a thread to
it. The answer lands on the expectation, survives the next thirty nightly
runs as revisions accumulate, and is one click away the next time the same
check goes red — instead of being a Slack message from March.

The same threads are readable by agents through
[Comma's MCP server](/mcp), so a data agent that owns the suite can pick up
"this one is expected until the CRM migration lands" without anyone
re-explaining it.

## Scheduling it

- **From your orchestrator** — an Airflow task, a dbt post-hook, or a
  [CI job](/docs/ci). It's plain HTTPS.
- **Or from a routine** — a [scheduled report](/features/routines/scheduled-html-reports)
  keeps the cadence next to the output.
- **Announce failures** — a [webhook](/docs/api) on `revision.created`
  posts the run into Slack, so a red night is a notification rather than a
  discovery.

## Try it

Comma is free — unlimited reports, unlimited commenters, unlimited revision
history. Publish tonight's checkpoint and send the link.

**[Create your first report →](https://commareports.com/)**

### Related

- [Share dbt docs](/share-dbt-docs) — the neighbouring problem in the same stack
- [Share an EDA report](/share-eda-report) — profiling output, same publish pattern
- [Comment on an HTML report](/comment-on-html) — how anchored threads work
