A weekly report can contain plausible numbers and still be unsuitable for a decision. A missing support export or an old CRM snapshot changes what the report can honestly say.
I built a small reporting demo for MadeBy.Expert to make those conditions visible. It uses synthetic billing, CRM and support snapshots, deterministic calculations and a template narrative. There are no live integrations or language model calls in this version.
Define the numbers before automating the report
The sample distinguishes monthly recurring revenue from revenue collected during the week. Open pipeline is the unweighted value of open opportunities, not a revenue forecast. Support metrics distinguish unresolved tickets at the snapshot from tickets resolved within the reporting period.
Each metric includes a source reference and a definition. Those details give the reviewer something concrete to check against the input.
For a client workflow, I would agree on these definitions before connecting the source systems.
A missing source is a report state
The browser demo lets you switch between complete sources, missing support data and stale CRM data. The report keeps the metrics it can calculate, omits the unavailable ones and records a blocker. It does not substitute zero for a missing value.
The final state comes from that blocker list:
status: blockers.length ? "blocked" : "draft",
For this example, all three sources are required for approval. A snapshot is stale if it was captured before the reporting period ended or more than 24 hours before the report's asOf time. A future timestamp is rejected as invalid input.
Those are choices for this workflow. A daily operations report might need a tighter freshness limit; another report might be useful with an explicitly optional source. The thresholds need agreement with the person using the result.
Here is the actual test covering all three sources and both failure modes. It uses Node's built-in test runner and strict assertions. fixture() loads the synthetic input afresh for each case:
test("missing/stale sources are unavailable and block approval", () => {
for (const kind of ["billing", "crm", "support"])
for (const mode of ["missing", "stale"]) {
const input = fixture();
if (mode === "missing") delete input.sources[kind];
else input.sources[kind].capturedAt = "2026-09-12T08:00:00Z";
const r = buildReport(input);
assert.equal(r.status, "blocked");
assert.ok(r.blockers.some((b) => b.startsWith(kind + ":")));
assert.equal(
r.metrics.some((m) => m.sourceId === fixture().sources[kind].id),
false,
);
assert.throws(
() => approveReport(r, r.digest, "Demo reviewer"),
/Blocked/,
);
}
});
The last assertion matters: reporting the problem in the output is insufficient if the approval function still accepts it.
Approval applies to one version of the report
The implementation computes a SHA-256 digest of the report content, which includes a digest of the input. A reviewer supplies the report digest when approving a local export. The approval function recomputes it and checks that the report is still a draft without blockers:
export function approveReport(
report,
digest,
reviewer,
now = new Date().toISOString(),
) {
const { digest: stored, ...content } = report;
if (hash(content) !== stored || digest !== stored)
fail("Approval digest does not match the current report");
if (report.status !== "draft" || report.blockers.length)
fail("Blocked reports cannot be approved");
if (
typeof reviewer !== "string" ||
reviewer.trim().length < 2 ||
reviewer.length > 100 ||
/[\r\n]/.test(reviewer)
)
fail("A named reviewer is required");
return {
...report,
status: "approved",
approval: { reviewer: reviewer.trim(), at: now, digest },
};
}
Here, hash is SHA-256 over JSON.stringify(value) and fail throws an error. This is an excerpt from the local prototype, not a standalone approval service.
Rebuilding the report after changing the input produces a different digest, so the previous digest cannot approve the new report. The tests also alter a calculated metric directly and check that approval rejects the modified content.
There are limits to this approach. The digest is a consistency check, not a signature or proof of who reviewed the report. The reviewer name is supplied as text; there is no authentication or authorization layer. The JSON serialization is also specific to this implementation, not a canonical format for independent producers.
A deployed approval service would need to establish the reviewer's identity and permissions, store the review event, and bind the delivery action to the approved version. Those parts are not implemented here.
The browser page shows the data conditions and report. The local runnable example handles approval and export; neither sends a report automatically.
Where AI could fit later
A language model might help draft commentary from validated facts. That is a possible extension, not an implemented feature of this demo. I would keep metric calculation, source checks and the approval boundary explicit regardless of how the narrative is produced.
The current prototype makes no claim about client time savings, live connector reliability or model quality. Those need measurement against an actual workflow.
Try the demo, including the missing-source and stale-source cases.
If your team assembles a similar report manually, describe the workflow: which tools supply the data, how often the report is needed, and who checks it. We can discuss whether a small paid pilot with clear success criteria fits.
Top comments (0)