The compatibility report, and why exit 2 is a feature
Every spreadsheet-to-PDF converter has limits. Most of them keep the list private, and you find out which limit you hit when a client asks why a chart is missing from page four.
Rendlio Sheets’ answer is a file. Every conversion — and every diagnose run — emits one JSON
report that answers three questions: what was in this workbook, what did Rendlio Sheets do with
each thing, and what should you check? It exists to enforce a single engine-wide rule:
nothing is ever silently approximated. Every deviation from faithful rendering is a
report entry with a location.
This post is the short tour. The normative version is the compatibility report schema.
It is an API, not a log file
The report is a public, versioned JSON document with a published JSON Schema, not human-readable output that happens to be parseable. Three consequences that matter in practice:
- The envelope is strict.
additionalProperties: falseat every level, so a report never carries an undocumented field you might come to depend on. - It is additively versioned.
schemaVersionis a semver-like1.x; new optional fields bump the minor, anything else bumps the major and gets a new schema URL. A validator pinned to v1 keeps working across engine updates. - Output ordering is deterministic. Identical runs produce byte-identical reports, so a report is a legitimate thing to check into a repository and diff.
You get it with --report <path>. Programmatic hosts should always set it; --report -
gives you a summary on stderr instead, which is the interactive-use shape.
Four statuses, and the one that matters
The heart of the report is features[]. Each entry names a stable feature id, how many
occurrences it covers, up to twenty locations, and one of exactly four statuses:
| Status | What it means |
|---|---|
rendered | Faithful, per the specification rule for that feature. |
approximated | Rendered, but a documented deviation applies — detail says which one. |
unsupported | Present in the file and not rendered; the space it would occupy follows the specified rule for that feature. |
skipped | Present but intentionally outside conversion semantics — not a fidelity gap. A VBA project is skipped, never executed. |
There is one reporting asymmetry worth internalising, because it changes how an empty report
reads: rendered entries are recorded only in diagnose mode. A convert-mode report
lists deviations only, which keeps it small. So in a convert report, an empty features
array does not mean the report failed to populate — it means nothing deviated.
The status that deserves your attention is approximated. unsupported is loud by
construction; approximated is the case where you did get a rendered document and something
in it followed a documented rule rather than the workbook. The detail string states which
rule applied.
The exit code is that verdict as a number
You do not have to parse anything to gate on the report. One ladder derives both the
report’s result.status and the process exit code, and the two are always in agreement:
if internal bug → error / 5elif resource limit hit → resource-limit / 4 (outputs deleted)elif input invalid (not xlsx, encrypted) → invalid-input / 3elif any feature status == unsupported → unsupported-content / 2elif any warning OR any approximated → warnings / 1else → success / 0| Code | Meaning |
|---|---|
| 0 | clean |
| 1 | rendered with warnings |
| 2 | unsupported content present |
| 3 | invalid input |
| 4 | resource limit hit |
| 5 | internal error |
Why exit 2 is a feature
Exit code 2 is the one that surprises people, so it is worth being blunt about what it is
not: it is not a failure. The document was written and it is complete. What the 2 tells
you is that the workbook contained something Rendlio Sheets does not yet render, and that
features[] names it, with a location.
Read it the other way around. A converter that returned 0 there would have made the same decision — it cannot render that chart type either — and simply not mentioned it. You would have shipped the same PDF, minus the knowledge that a chart is missing from it. The 2 is not Rendlio Sheets doing less; it is Rendlio Sheets telling you what every converter in this category is already doing quietly.
That is why 2 is a review this signal rather than a broken build. Most pipelines treat
<= 1 as pass and 2 as review:
rendlio sheets convert workbook.xlsx -o out.pdf --format pdfa --report report.jsoncode=$?if [ "$code" -gt 1 ]; then echo "conversion not clean (exit $code) — see report.json" >&2 exit "$code"fiWhere a policy needs to be sharper than the exit code, assert on the report itself. Fail on any unsupported content, but print what it was first:
jq -r '.features[] | select(.status == "unsupported") | "\(.id) ×\(.count): \(.detail // "see docs")"' report.jsonjq -e '[.features[] | select(.status == "unsupported")] | length == 0' report.jsonCodes 4 and 5 are the real failures. On a resource-limit hit the partial outputs are deleted before the process exits, so a pipeline can never gate on a truncated PDF — and the report is still written, naming the limit, its configured value, and the measured one.
Two things the report will never contain
- Cell contents.
detailandmessagestrings are static templates plus counts and locations. CI includes a leak test — fixtures with sentinel strings in their cells assert the sentinels never reach the output — so a report from a confidential workbook is safe to attach to a support mail. This is also what makesrendlio sheets diagnoseusable on a workbook you are not allowed to send anywhere. - Licence data. Beyond
options.licenseMode, which is eitherlicensed:<tier>orevaluation, no licence information appears in a report.
Where the contract lives
- Compatibility report — the v1 schema field by field, the warning-code ranges, an annotated example, and three levels of CI gating.
- Quickstart — the five-minute path from install to a first PDF with a report next to it.
- Security model — what happens when the workbook is not merely awkward but hostile.