Engineering NotesTools and execution
Block Office files that won't open, then show the model its pages

When a model writes code that builds an Excel file, the run can exit with code 0 and the model can answer "Done," while Excel refuses to open the file. If a LET formula's parameter names are missing the _xlpm. prefix, the generation library saves without complaint and Excel won't open the workbook. A file that opens can still be wrong. When the title on slide five overflows its box, nothing in the file structure is broken. The model can't open its own file, so it notices neither problem.
The two kinds of defect show up in different places. Whether a file opens is visible only in its XML; whether the layout is right is visible only when the page is drawn. So we split the check in two. Validators read the file structure, and a file with a defect that keeps it from opening never reaches the user. A file that passes is rendered as page images and attached to the model's next input, and the model decides from the pictures whether to fix anything. This post covers how the validators differ by format, and why the visual check is not a tool the model calls.
Defects that stop a file from opening are invisible on screen, and validators can't see overflowing text
Word, PowerPoint and Excel files are zip archives of XML parts plus relationship definitions between them. Most defects that make an app reject a file live in that XML.
- When a string pulled from a database or a PDF contains a control character such as a vertical tab, the part is no longer well-formed XML and the app won't open the file. Some generation libraries escape only the five XML special characters and write control characters as they are. So the tool strips these characters before validation (step 3 below).
- When a slide part's content type declaration is missing, the package default fills the gap and the type becomes
application/xml. Both a public schema validator and python-pptx passed that file. - Excel strips formula prefixes such as
_xlpm.and_xlfn.from what it displays, so you can't tell by looking that one is missing. A cell missing_xlfn.opens but shows#NAME?.
Overflowing text, overlapping shapes and empty slides, on the other hand, happen in perfectly valid XML. Neither check alone catches the other's defects.
There are three common approaches. If the model writes file contents directly as tokens, a one-character XML mistake becomes a file that won't open. If one general-purpose OOXML validator is the pass condition, depending on the format it either catches nothing or blocks healthy files (more on this below). If the model is told "call the check tool when you're done," it skips that step. The measurement showing that an instruction to load the task guide first was almost never followed is in “Tools copy the form and load the guide; models fill the arguments”.
The model writes code, and the server checks that the file really exists
Instead of file contents, the model writes code that uses a document library. For new files that is pptxgenjs for PowerPoint, docx-js for Word, openpyxl for Excel and python-hwpx for Hangul. Code becomes a file in these steps.
- Run. The code runs in an isolated execution sidecar and writes its output straight to the sandbox volume.
- File check. Exit code 0 is not evidence of success. If the model catches an error with
catchand only prints it, the failure still exits with 0, and if the code doesn't wait for the save, the process ends before the file is written. So the check looks at whether the file actually exists. - Character cleanup. The tool strips characters XML cannot carry (control characters other than tab and newline, noncharacters such as U+FFFE) from the XML parts of the finished file. The file opens even when a generation library wrote such characters as they were. The stripped characters are gone from the text, so the work guide tells the script to clean text whose layout matters before writing it. For Excel, openpyxl rejects such a character the moment it goes into a cell and the script stops, so the guide supplies code that filters it out beforehand.
- Normalization. PowerPoint only. It fixes defects the generation library leaves in the same place every time (chart axis counts, element order and the like).
- Validation. A per-format validator finds problems and grades them. If even one is fatal, the file does not go to the user.
On a fatal defect, the tool returns a failure that carries each problem's location and how to fix it. For broken XML, that includes which part and which line and column. The model reads this, fixes its code and builds the file again. If a file with the same name was already at that path, the original is put back. A file handed to the user earlier points at that path, and leaving the rejected output there would make the user download the rejected file.
We trust a general-purpose validator differently for each format
Every format first goes through XML parsing and package checks (content types, and whether each relationship points at a part that exists). Parsing is the only check that reports where the XML broke. The public schema validator (openxml-audit, a Python port of the Open XML SDK validation logic) also notices the breakage but doesn't say which line. After that, each format differs.
| Format | Schema validator | Examples of in-house checks |
|---|---|---|
| PowerPoint | Used, minus two known errors | Chart axis references, embedded workbooks |
| Word | Used | Missing author on tracked changes, paragraph property order |
| Excel | Not used | Formula prefixes, shared string indexes, dynamic array formulas, manual calculation mode |
| Hangul (hwpx) | Separate validator | Container conventions, whether the body part is reachable |
For PowerPoint, normalization cuts schema errors. A three-slide deck produced 7 schema errors, and 2 remained after normalization. The remaining 2 concern the order of child elements in the presentation part. Reordering them to match the ECMA standard takes validator errors to 0, but PowerPoint 16.0 could not open that file. The file in its original order opens normally. So we leave those 2 unfixed and drop them from the results, but only when both appear together in exactly that shape. Errors on the same element names also come from other ordering mistakes, so if anything differs, every error is reported.
Excel gets no schema validator. In a side-by-side comparison, that validator caught none of the 9 kinds of defects that mattered. It gave the same diagnosis for a file Excel refuses to open (missing _xlpm.) and for a healthy one, and it flagged every file openpyxl produced for element order in the styles part. A check that can't tell healthy files from broken ones can't be a pass condition, so Excel is judged by in-house checks alone. Dynamic array formulas are an example. openpyxl has no API for writing spill metadata, so the result fills only the top-left cell and the error count is 0. The in-house check catches this.
Hangul documents are built as hwpx. hwpx is not OOXML but an OCF container from the ODF family, with no content-types part and no relationship files. So it has its own validator, and the two share only the shape of a result and the rule "block if fatal." Schema warnings don't block. The published OWPML schema diverges in places from how Hancom Office actually behaves, so the library itself reports schema violations as warnings. Four real hwpx files (2 government forms, 2 internal reports) and newly created documents all produced 0 schema findings.
Only files that won't open are blocked; everything else goes to the model
Problems a validator finds fall into four grades.
| Grade | Meaning | Handling |
|---|---|---|
| Fatal | The app can't open the file or shows a repair dialog | Not delivered |
| Other-app risk | The main app opens it, but other apps such as LibreOffice or Keynote break | Delivered, model is told |
| Silent error | It opens with no error, but the content differs from the intent (#NAME?, an array formula that fills only the top-left cell) |
Delivered, model is told |
| Tolerated | Violates the spec, but every app we checked opens it | Logged only |
The blocking rule is one question: does the main app open this file? Problems below fatal don't block, because the file opens; each one goes back to the model in the tool result with its grade, location and how to fix it. For a workbook left in manual calculation mode, that means the fact "Excel won't recalculate and shows stale values" plus the instruction to remove the setting. The model that reads the result decides whether to fix it.
Edits are compared with a copy of the original for new problems and lost structure
When the model edits a file someone else provided, the decision is not based on how many problems it has. When we checked 12 internal templates, 9 (6 contracts, 3 presentation templates) had fatal defects before anyone touched them, and one had 411. Blocking on the count would roll back even a perfect edit. So before the edit, a copy of the original goes through the same validator, and only problems that appear after the edit are counted. Problems that were already there don't block, but the model gets a one-line note of how many there were. Without it, the model would take those problems for its own mistakes and fix the wrong things. The note is a summary rather than a list so that hundreds of entries don't fill the context.
Some losses are invisible to a validator. Edit code can delete part of the original's structure and still leave valid XML. In python-docx, the property that sets a cell's text deletes not only the paragraphs in the cell but any table inside it as well. A document that lost two nested tables (17 options) from its value cells this way produced 0 validation findings. A schema says what must be there, not what was there and is now gone. So the structure node counts are compared with the same copy of the original: tables, rows and paragraphs for Word, and slides, paragraphs and text runs for PowerPoint. Character counts are not used, because filling in a template naturally adds text. Excel is not counted either. openpyxl rewrites the whole workbook on save, so even a normal edit would look as if structure had shrunk. Lost structure does not block; the model gets the before and after counts. "Delete this table" can be a legitimate instruction, and the model is the one that knows the instruction.
Rendered pages are attached to the next round without the model asking
A file that passes validation can still ship with overflowing text or overlapping shapes. So when a round (one model call plus running the tool calls in its response) succeeds in writing an Office file, the render sidecar draws the file as page images, and those images are attached after the tool results as input to the next call. The model has already seen the pictures and only decides whether to fix something. This step always runs when the model can read images. How rendering works is in “Model-drawn bars can miss their numbers, so the screen draws charts”.
The number of pages shown has two settings.
| Target | Default pages | Config key |
|---|---|---|
| A file the model created in this turn | 30 | officeReview.createdMaxPages |
| A file the user provided, when editing or reading it | 3 | officeReview.maxPages |
When only the first 3 pages of a new file are shown, the model says it is done without ever seeing the overflow and overlaps from page 4 on. It just made the file and has the code to fix it, so defects on pages it wasn't shown never get caught. A contract the user provides can run to dozens of pages, and drawing all of them fills the context with images. For edits, pages are compared pixel by pixel with the pre-edit render and only changed pages are shown, with the first few shown before and after side by side.
Each time the same file is written again, it is shown again. In our measurements the model rewrote a file 3.22 times on average, so showing each file only once would usually leave the last result unchecked. The concern about an endless render-and-fix cycle is already covered by the turn's maximum iteration count and time budget.
Design choices
We leave 2 schema errors in PowerPoint files. Aiming for zero validator errors produces files PowerPoint refuses. The cost is that zero validator errors can't be the pass condition, and the 2 we drop are accepted only in their exact shape.
Dropping the schema validator for Excel means the in-house checks carry the decision alone. They don't catch kinds of defects they don't check for, so each newly found defect needs a new check. We judged that better than keeping a check that gives the same answer for healthy and broken files.
The blocking rule is a single question: does the main app open it? The cost is that files that break in other apps, or that have some #NAME? cells, go out as they are unless the model fixes them. Not blocking on hwpx schema warnings follows the same rule. Blocking where the public schema and Hancom Office disagree would stop documents that open fine in Hangul over specification gaps OwlAI did not create.
Edits are judged against the original. A template that already had defects passes the edit, and those defects stay in the output. Whether to ship an edit whose structure shrank is also left to the model.
Always attaching the visual check costs context. Page images grow the input every round, so the page budget goes to new files the model can fix, and files someone else provided are cut to 3 pages.
The validators answer only whether a file opens. Whether the numbers in a table are right is settled by the SQL that queried them. If your agent builds files by writing code, a good place to start is to have the server check that the file exists and opens, and to hand the rendered pages back to the model as pictures.
What problem does this solve?
More in Tools and execution

Engineering Notes · Tools and execution
Rerunning a chat repeats its mistakes. Keep the steps and working SQL
Say a chat once went well: it pulled last week's sales from the database, grouped them by department, and built an Excel file.

Engineering Notes · Tools and execution
Tool calls a script makes still get permission and approval checks
Take a request like "For each issue opened last week, look up the customer's contract, and email the owner onl…

Engineering Notes · Tools and execution
Tools copy the form and load the guide; models fill the arguments
Making a quote from a company form takes two steps: copy the form to a new path, then fill in the copy.
