Dev.to AI 🤖 Ai 👁 0 📖 4 min read

Your Agent Wrote a Great Report. Now Give Me the Actual File.

Agents are very good at producing text. Users, annoyingly, want files. Nobody's boss has ever said "great work, paste the chat transcript into the board pack." They want a Word document, a spreadsheet they can sort, a d

Agents are very good at producing text. Users, annoyingly, want files.

Nobody's boss has ever said "great work, paste the chat transcript into the board pack." They want a Word document, a spreadsheet they can sort, a deck they can present and a PDF they can email to someone who refuses to open anything else.

So at some point every agent product needs an export step. The tempting version is to send everything to a server, render it there and send a file back. It works. It also means another service to run, another place user data gets copied and another thing to bill for.

There's a simpler option for most cases. Build the files in the browser. The libraries are mature, the files never leave the user's machine and your server never has to know what was in the quarterly numbers.

Here's how I'd set it up.

Don't let the model write the file

The first rule is the most important one. The model should never produce the file format directly. It should produce structured data, and your code should turn that data into a file.

Ask the model for JSON that matches a schema you control:

{
  "title": "Weekly Sales Report",
  "sections": [
    { "heading": "Summary", "paragraphs": ["Revenue rose 8% week on week."] },
    { "heading": "Stalled deals", "table": {
        "columns": ["Deal", "Owner", "Days stalled"],
        "rows": [["Acme renewal", "Sam", 12], ["Globex pilot", "Priya", 9]]
    }}
  ]
}

Then every exporter reads the same structure. Word, PDF, Excel and PowerPoint become four small rendering functions instead of four separate prompts. When the model has a creative day, your schema validation catches it before it reaches a file.

A tiny download helper

Every format below ends up as a Blob. One helper saves them all:

function downloadBlob(blob, fileName) {
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = fileName;
  document.body.appendChild(a);
  a.click();
  a.remove();
  setTimeout(() => URL.revokeObjectURL(url), 1000);
}

Revoking the URL a moment later frees the memory without racing the download.

Word with docx

The docx package builds real .docx files with headings, tables and styles.

import { Document, Packer, Paragraph, HeadingLevel } from "docx";

async function exportWord(report) {
  const children = [new Paragraph({ text: report.title, heading: HeadingLevel.TITLE })];

  for (const section of report.sections) {
    children.push(new Paragraph({ text: section.heading, heading: HeadingLevel.HEADING_1 }));
    for (const p of section.paragraphs ?? []) {
      children.push(new Paragraph(p));
    }
  }

  const doc = new Document({ sections: [{ children }] });
  downloadBlob(await Packer.toBlob(doc), `${report.title}.docx`);
}

Tables take a little more code, but the pattern is the same. Map your structure to the library's objects and let it handle the XML.

Excel with SheetJS

For spreadsheets, SheetJS turns an array of rows into a workbook in a few lines.

import * as XLSX from "xlsx";

function exportExcel(table, fileName) {
  const ws = XLSX.utils.aoa_to_sheet([table.columns, ...table.rows]);
  const wb = XLSX.utils.book_new();
  XLSX.utils.book_append_sheet(wb, ws, "Data");
  XLSX.writeFile(wb, fileName);
}

One gotcha: install SheetJS from the project's own CDN as its docs describe. The copy on the public npm registry is several years out of date.

Keep numbers as numbers. If your model returns "12 days" instead of 12, the user gets a column they can't sort, and they will tell you about it.

PowerPoint with PptxGenJS

PptxGenJS is the friendliest way to make slides from code.

import pptxgen from "pptxgenjs";

async function exportDeck(report) {
  const pptx = new pptxgen();

  const cover = pptx.addSlide();
  cover.addText(report.title, { x: 0.5, y: 2, w: 9, fontSize: 36, bold: true });

  for (const section of report.sections) {
    const slide = pptx.addSlide();
    slide.addText(section.heading, { x: 0.5, y: 0.4, fontSize: 24, bold: true });
    if (section.paragraphs) {
      slide.addText(section.paragraphs.join("\n"), { x: 0.5, y: 1.3, w: 9, fontSize: 16 });
    }
  }

  await pptx.writeFile({ fileName: `${report.title}.pptx` });
}

A useful rule for agent-generated decks: one section per slide, and cap the text. Models love to write paragraphs. Audiences love to read nothing.

PDF with jsPDF

jsPDF is fine for simple reports.

import { jsPDF } from "jspdf";

function exportPdf(report) {
  const pdf = new jsPDF();
  pdf.setFontSize(20);
  pdf.text(report.title, 15, 20);
  // add sections, wrapping long lines with pdf.splitTextToSize(text, width)
  pdf.save(`${report.title}.pdf`);
}

Watch out for non-Latin text. The built-in fonts don't cover scripts like Japanese or Arabic, so if your agents work in more than one language you'll need to embed a font that does. This is the bug that only appears after launch, in the one language nobody on the team speaks.

Keep the bundle sane

These libraries are not small. Don't ship all four on page load. Load each one when the user actually clicks export:

exportButton.addEventListener("click", async () => {
  const { exportWord } = await import("./exporters/word.js");
  await exportWord(currentReport);
});

Most users export one format, most of the time. Dynamic imports mean they only download that one.

Why bother doing it client-side

Three reasons, in order of how often they come up.

Privacy. The report never touches your server, which is easy to explain and easy to put in a privacy policy.

Cost. File generation is CPU work. Letting the user's laptop do it is the cheapest compute you'll ever get.

Simplicity. No queue, no storage bucket, no "your file is ready" email. The user clicks and the file appears.

There are still good reasons to render on a server, such as very large files, strict corporate templates or scheduled reports. For everything else, the browser is quietly very good at this.

Users don't remember that your agent wrote a great summary. They remember that the file opened in Excel on the first try.

I'm Head of SI Agents at Magnus, where agents can export their work as Word, Excel, PowerPoint and PDF files. This post was drafted with help from an SI writing assistant and edited by me.

📰 Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.