# Share an HTML Report From Argo Workflows

Canonical: https://commareports.com/ci/argo-workflows-html-report
Published: 2026-09-02

> Argo artifacts live in your object store behind the cluster, and the Argo UI is usually behind a VPN. Publish the report from an exit handler so people outside Kubernetes can read it.

# Share an HTML report from Argo Workflows

Argo runs the pipeline that produces the report. It does not solve the part
after that, and on Kubernetes the part after that is harder than usual.

- **Artifacts live in your object store.** S3, GCS or MinIO, reachable by the
  workflow controller. A human who wants to read `report.html` needs
  credentials to that bucket, or a person with credentials to fetch it for
  them.
- **The Argo UI is behind the cluster.** It is normally exposed on an internal
  ingress, or through `kubectl port-forward`, or not at all. The analyst
  waiting on the nightly pipeline is not going to port-forward.
- **HTML artifacts download rather than render.** Argo serves artifacts with
  headers chosen so a workflow cannot script the Argo UI — the right call, and
  it means your report arrives as a file, not a page.

So the result of a pipeline that ran on forty pods gets screenshotted by the
one person with cluster access and pasted into Slack.

## Publish from an exit handler

Put a [scoped token](/docs/api-tokens) (`reports:write` only) in a Kubernetes
Secret, create the report once, and `PATCH` that id from the workflow's
`onExit` handler:

```yaml
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: nightly-eval-
spec:
  entrypoint: pipeline
  onExit: publish-report
  arguments:
    parameters:
      - name: report-id
        value: rep_xxxxxxxx
  volumeClaimTemplates:
    - metadata: { name: work }
      spec:
        accessModes: ["ReadWriteOnce"]
        resources: { requests: { storage: 1Gi } }

  templates:
    - name: pipeline
      container:
        image: ghcr.io/acme/eval:latest
        command: [bash, -c]
        args: ["python -m eval --html /work/report.html"]
        volumeMounts: [{ name: work, mountPath: /work }]

    - name: publish-report
      container:
        image: ghcr.io/acme/curl-jq:latest
        command: [bash, -c]
        args:
          - |
            curl -fsS -X PATCH \
              "https://commareports.com/api/v1/reports/{{workflow.parameters.report-id}}" \
              -H "Authorization: Bearer $COMMA_API_TOKEN" \
              -H "Content-Type: application/json" \
              -d "$(jq -n --rawfile html /work/report.html \
                    --arg title "Nightly eval — {{workflow.name}} ({{workflow.status}})" \
                    '{title: $title, html: $html}')"
        env:
          - name: COMMA_API_TOKEN
            valueFrom:
              secretKeyRef: { name: comma-api, key: token }
        volumeMounts: [{ name: work, mountPath: /work }]
```

Three details carry it:

- **`onExit` runs either way.** A workflow that failed halfway is exactly the
  workflow whose report someone wants, and an exit handler is the only place in
  Argo that reliably runs for both outcomes.
- **`{{workflow.status}}` in the title** means the reader can see from the
  report list whether that run finished, without opening anything.
- **The token is a `secretKeyRef`, not an env literal**, so it is not sitting
  in the Workflow spec that anyone with `get workflows` can read.

Your artifact repository stays exactly as it is. Keeping the raw outputs in
the bucket costs nothing; the published report is for the people who are not
going to `kubectl` anything.

## What the published copy adds

- **A URL outside the cluster.** No VPN, no port-forward, no bucket
  credentials — visibility is
  [private, team, domain-gated, or link](/docs/sharing), decided per report.
- **It renders.** HTML is stored verbatim and served in a sandboxed iframe with
  scripts enabled, so a plotly chart or a filterable table still works.
- **One URL across every run.** Each run appends a revision at the same
  address, and any two revisions can be diffed — which is what makes a nightly
  pipeline legible over time.
- **Comments anchored to the content.** The analyst highlights the metric that
  moved and pins a thread to it — see
  [commenting on HTML](/comment-on-html).
- **Announcements.** A [webhook](/docs/api) on `revision.created` posts each
  run into Slack.

## Limits

- **HTML body: 5 MB.** Larger outputs go in as [assets](/docs/api) at 25 MB per
  file, 250 MB per report.
- **A report that fetches sibling data files at view time** can't, from the
  sandbox — publish a static digest with the archive attached, as in
  [sharing an Allure report](/share-allure-report).
- **Rate limit: 60 requests/minute per token.** A fan-out workflow that
  publishes from every parallel pod should publish once from the exit handler
  instead.

## Try it

Comma is free — unlimited reports, unlimited commenters, unlimited revision
history.

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

### Related

- [Publish from CI](/docs/ci) — the general pipeline pattern
- [Share a Nextflow report](/share-nextflow-report) — the other
  pipeline-on-a-cluster case
- [Share an LLM eval report](/agents/share-llm-eval-report) ·
  [Scheduled HTML reports](/features/routines/scheduled-html-reports)
