How to Use IronXL with ChatGPT Codex
A shared inbox folder collects whatever the branches send. Modern XLSX exports land beside XLS files from systems retired years ago, comma and semicolon CSV files, tab-separated warehouse dumps, an encrypted payroll workbook, a web portal export that turns out to be HTML, and the occasional zero-byte file left by a failed upload. The warehouse wants every one of them as CSV, JSON and XML, and it wants to know which files did not make it.
ChatGPT Codex is OpenAI's coding agent. It ships as a terminal program, as IDE extensions, inside the ChatGPT desktop app and as a cloud service, and each of them can sign in with the same ChatGPT account. IronXL is Iron Software's spreadsheet library for .NET.
Each conversion prompt in this guide makes Codex write a results file with one row per input file before it reports anything, and every conversion figure here was counted from a file of that kind.
QuickstartOpen Codex in a .NET console project with some sample spreadsheets in it, and describe the conversion.
$ironxl Add IronXL to this console project and have Program.cs convert every file in Samples/.
- Save each worksheet as CSV, JSON and XML under out/quickstart/<file name>/.
- Log each input file as one line in out/quickstart-results.csv.
- Give each line the sheet names, data rows per sheet and output files.
- For a file that fails, add the exception type to its line.
- Read that CSV back and print how many files succeeded and how many failed.
The license key is in IRONXL_LICENSE_KEY.
Build and run it, and fix whatever breaks.
The $ironxl mention loads IronXL's skill before Codex writes code.
Minimal Workflow (5 steps)
- Add the IronXL.Excel NuGet package to the project
- Save IronXL's skill as
.agents/skills/ironxl/SKILL.mdso the$ironxlmention resolves - Commit an
AGENTS.mdthat makes IronXL the only spreadsheet library and keeps keys and passwords out of every output - Check each file's leading bytes, separator and password before IronXL opens it, and record a status for every input
- Merge the modes into one tested job and look up every member it calls in IronXL's API reference
What is ChatGPT Codex, and how does it fit into .NET spreadsheet work?
Codex carries out the work it is given, within the permissions you grant. For a conversion pipeline it writes the converter, runs it over the inbox, opens the files it produced, and edits the code when they are wrong. Install the CLI with npm install -g @openai/codex from the Codex repository, and see OpenAI's Codex CLI pages for the terminal.
OpenAI's security guidance asks for the same targeted checks and diff review on Codex output that a human pull request would get. In this pipeline an invented member breaks the build, and a bad conversion shows up as a results row that does not match the manifest.
Codex may lay out the converter differently each time, but IronXL has to return the same sheets and values for a given file. WorkBook.Load opens XLS, XLSX, XLSM and XLTX files as well as CSV and TSV, as the load guide lists.
How do you write an IronXL prompt and set up Codex for it?
A conversion prompt states which folder to read, how each file type is opened, where every output goes, what one row of the results file holds, and what counts as proof. Any of those left out becomes a decision the agent makes for you.
| Component | Weak prompt | Strong prompt |
|---|---|---|
| Input | Convert the spreadsheets | Every file in Inbox/, XLS, XLSX, CSV and TSV |
| Loading | Unstated | WorkBook.LoadCSV with the separator from out/inventory.csv |
| Output | Save them as CSV | CSV, JSON and XML per sheet, in one folder per input file under out/converted/ |
| Errors | Unstated | Any file that throws gets a results row with its exception type |
| Proof | Check it worked | Every row of out/convert-results.csv agrees with Expected/manifest.csv |
| Constraints | None | Leave Inbox/ untouched. No password in any output. |
OpenAI's research on hallucination finds that training and evaluation reward guessing, and in a conversion job a guess can surface as a total in the closing summary.
Codex looks for skills in .agents/skills folders from the working directory up to the repository root. A skill is a folder whose SKILL.md starts with a name and a description. IronXL publishes its skill.md with the name ironxl, so saving it as .agents/skills/ironxl/SKILL.md makes $ironxl resolve. Without the mention, Codex can still choose the skill on its own if the task fits the skill's description, and IronXL's description lists conversions between XLSX, XLS, CSV, TSV, JSON and XML.
AGENTS.md holds the standing rules. Codex collects these files starting at the repository root and walking down to the folder where the session began, and where two files conflict, the one closer to that folder takes precedence.
Write a root-level AGENTS.md with a section called Spreadsheet conversion.
It should hold these rules:
- IronXL (NuGet IronXL.Excel, namespace IronXL) is the only spreadsheet library allowed.
- API names come from the ironxl skill in .agents/skills/ironxl/.
- The index at ironsoftware.com/excel/csharp/llms.txt is also a source for them.
- IRONXL_LICENSE_KEY holds the license key, which is never echoed or saved.
- Inbox/, Expected/ and config/ are read-only.
- Passwords from config/known-passwords.csv never reach source code, an output or a log.
- Only a per-file results file written in that turn can back a count, total or timing in a reply.
- A reply that gives such a number names that file.
Expect one new section in AGENTS.md and a reply with no numbers in it, since this turn converts nothing.
How do you ask Codex to add IronXL and convert a first folder?
Check the package before anything else.
Tell me about the IronXL package, and don't change any package reference.
1. Print the IronXL.Excel PackageReference from SheetConvert.csproj and the version restore resolved.
2. Run dotnet list package --include-transitive and copy out every line that isn't IronXL.Excel itself.
3. Say whether anything in that graph needs Microsoft Office, Office Interop or a native library on Linux.
IronXL.Excel 2026.10.1 depends directly on IronSoftware.Common 2026.4.2 and IronSoftware.System.Drawing 2026.8.2. On .NET 9 the transitive list held 30 packages besides IronXL.Excel itself, none of them Office or Interop, and every DLL in the build output, including the platform folders under runtimes, was a managed assembly. Keep the version Codex reports for later prompts, and see the IronXL changelog when picking a release.
Without a key, every IronXL load throws. Across a 28-file inbox that would mean 28 copies of the same failure, so the guard below exits before any file is touched.
Put the license setup in Startup.ApplyIronXlKey and call it before any other line of Program.cs runs.
If IRONXL_LICENSE_KEY is missing or License.IsLicensed is false, write one line to stderr.
Then exit with code 2 before opening any inbox file.
Run the program twice, once as it is and once with the variable unset for that run only.
Give me both exit codes.
The license keys page covers trial keys and the configuration files IronXL also reads.
Before the converter exists, find out what the folder holds. OpenAI documents Codex's built-in explorer as a read-heavy agent for codebase exploration, which suits a survey with no edits.
Ask the explorer agent to survey Inbox/ without changing anything.
For each file it should report:
- the extension and the size in bytes
- the first eight bytes in hex
- what those bytes say the file really is (OOXML zip, OLE2 compound file, plain text, HTML, or unknown)
- for a text file, the separator in its header line
When the explorer reports back, save its findings as out/inventory.csv with one row per inbox file.
Then list every file whose bytes disagree with its extension.
An XLSX is a zip archive that starts with PK, and an XLS is an OLE2 compound file that starts with D0 CF 11 E0. The survey flags a comma-separated text file named .xlsx and a portal export whose .xls holds an HTML table, and it cannot identify the zero-byte Newcastle file or a file of random bytes from their leading bytes. It also flags the two encrypted workbooks, because an encrypted XLSX is an OLE2 container with the same signature as a legacy XLS.
Create Services/InboxConverter.cs with a Convert(string path) method.
Convert returns a FileResult record.
The record holds the file, status, reason, sheet names and data rows per sheet.
It also holds the output paths, and the exception type and message.
Convert follows these rules:
- Open .xls and .xlsx files with WorkBook.Load.
- Open .csv and .tsv files with WorkBook.LoadCSV, using the separator recorded in out/inventory.csv.
- Save every sheet with SaveAsCsv, SaveAsJson and SaveAsXml into out/converted/<file name without extension>/.
- Keep going when a file throws.
Add a --convert mode that writes out/convert-results.csv.
Run it, and give me the status counts from that file.
Four of its rows have the wrong status at this stage. The zero-byte file and the text file named .xlsx count as converted, and the two encrypted workbooks count as failures, the payroll file whose password is known and a bonus pool file that belongs under Protected. Later prompts correct all four.
Where does IronXL write each worksheet?
A workbook save does not write to the path you pass. Called on a workbook whose only sheet is Orders, SaveAsCsv with the path out/x/orders.csv writes orders.Orders.csv, the file name followed by the sheet name, and never creates orders.csv. The conversion guide documents one file per worksheet for CSV, TSV, JSON and XML, and we measured the details a pipeline depends on with 2026.10.1.
- A single sheet still gets a suffix: A workbook loaded from CSV or TSV has one sheet called
Sheet0, so customers-uk.csv becomes customers-uk.Sheet0.csv. - Sheet names pass through unchanged: The inbox's Q3 Summary sheet gives a file name with a space in it, and a French sheet name keeps its accented letters, so downstream scripts must quote paths.
- The worksheet writers behave differently: A plain workbook save with a CSV, TSV, JSON or XML name also splits by sheet, while
WorkSheet.SaveAsCsvand the other worksheet writers produce exactly the path they are given. - Each format has its own shape: The CSV starts with a UTF-8 byte order mark, JSON is an array of rows with every value as a string, and XML gives each cell a ColumnN element with an
xsi:typesuch asxs:double. Sheet names that are not valid XML names are escaped, so Q3 Summary becomes the elementQ3_x0020_Summary.
Because the CSV can contain quoted line breaks and ends with an empty line, counting its lines gives the wrong row count. Count data rows from the worksheet itself, as the results file does, and list the output paths IronXL wrote.
Why does a converted CSV lose its leading zeros?
The team that imports the converted data may report a problem as a picture. Codex accepts images when a session starts with codex --image, and the CLI also lets you paste one into the composer.
I've attached a screenshot with codex --image.
The customer accounts team took it from their import preview of the converted customers-uk file.
1. Read the picture and compare it with Inbox/customers-uk.csv.
2. Tell me which values changed between source and output, and which IronXL setting changed them.
3. Fix the conversion and rerun it for that file.
4. Write out/accountno-check.csv with one row per customer.
Give each row of that file these columns:
- source AccountNo
- converted AccountNo
- whether they match
The screenshot shows three accounts the import could not find, 123, 4567 and 9. The source file has them as 000123, 004567 and 000009. LoadCSV has a convertFieldValues argument that defaults to true, and it turns any field that looks like a number into a number, which drops leading zeros. Passing convertFieldValues: false keeps every field as the text the branch wrote.
Why does a CSV load differently on another machine?
LoadCSV splits columns at its listDelimiter argument, and when that is null IronXL falls back to the list separator of the current culture. On our en-CA machine that separator is a comma, so a semicolon file from Lyon loaded as two columns cut at each decimal comma. Under de-DE the same call split it correctly. WorkBook.Load on a CSV file behaves the same way. The CSV reading guide shows the explicit form.
The separator is half the problem. With field conversion left on, IronXL parses numbers using the current culture as well. On en-CA, LoadCSV with a semicolon delimiter read the Lyon price 4,20 as 420, with no error, because a comma there is a thousands separator. Under de-DE it read 4.2.
Prove the text survived conversion for every .csv and .tsv in Inbox/.
1. Rerun --convert so every file goes through the fixed conversion.
2. Compare each source field with the same field in the converted CSV.
3. Write out/text-fidelity.csv with one row per file.
4. Load orders-lyon-2026-09.csv again, first under de-DE and then under en-US.
5. Use a semicolon separator for both loads, and leave convertFieldValues at its default.
6. Add two rows to out/text-fidelity.csv giving the UnitPrice of the first data row under each culture.
Give each per-file row these columns:
- separator
- source columns
- output columns
- fields compared
- fields that differ
- first difference
Which value does IronXL write for a formatted cell?
The workbook's CSV and JSON writers write the text each cell displays, the same string as its FormattedCellValue, so the number format decides what reaches the warehouse. The XML writer writes the stored value with its type. We measured this on an XLSX written by openpyxl and on the inbox's Derby XLS.
| Stored value | Format | CSV, en-CA | CSV, de-DE | XML |
|---|---|---|---|---|
| 1234.5 | #,##0.00 | "1,234.50" | 1.234,50 | 1234.5 |
| 2.345678 | 0.00 | 2.35 | 2,35 | 2.345678 |
| 0.256 | 0% | 26% | 26% | 0.256 |
| 123 | 000000 | 000123 | 000123 | 123 |
| 1 March 2018 | m/d/yy h:mm | 3-1-18 0:00 | 3.1.18 0:00 | 2018-03-01T00:00:00 |
Rounding drops digits and nothing reports it. A weight of 0.004 under the 0.00 format came out as 0.00. The culture reaches the output as well. The shipped XML documentation says to leave the delimiter of the CSV writer null for the system default, and under de-DE that default wrote semicolons. Passing a comma as the delimiter changed only the separator. A Derby price in the General format still came out as "33,84".
Under en-CA the inbox hides most of this, because its other workbooks keep numbers in the General format and dates as yyyy-mm-dd. The Derby file, written by IronXL, is the exception. In Codex's own CSV for it, all 70 dates came out like the last table row, a two-digit year with the en-CA date separator, where en-US gives 3/1/18 0:00. When the warehouse needs stored values, build the CSV from each cell's Value formatted with CultureInfo.InvariantCulture. That gave 1234.5, 2.345678 and 2026-09-20 under en-CA, de-DE and fr-FR alike.
Why can a broken file convert without an error?
When WorkBook.Load cannot read a file as a workbook, it tries again as CSV. That fallback explains several results in the inbox.
| Inbox file | Load result | Record as |
|---|---|---|
| Zero-byte .xlsx | One empty sheet, no exception | Unreadable, empty |
| Comma-separated text saved as .xlsx | Loaded as CSV, nine data rows in two columns | Unreadable, content-mismatch |
| HTML table saved as .xls | Threw FileLoadException. A copy with no quote characters loaded as one cell | Unreadable, content-mismatch |
| Random bytes saved as .xls | Threw FileLoadException, inner AggregateException "Excel load failed, then CSV fallback also failed" | Unreadable, unknown-format |
| First 40 percent of a real .xlsx | Threw FileLoadException citing insufficient memory, inner SharpZipBaseException "Unexpected EOF" | Unreadable, truncated |
The first two rows are the dangerous ones, because a converter that only catches exceptions reports them as successes and ships an empty sheet or a renamed text file to the warehouse.
Add a byte check to InboxConverter.
Expected/README.md defines when a file counts as Unreadable and which reason to record.
- Before IronXL opens a file, compare its leading bytes with its extension.
- On a mismatch, set the status to Unreadable.
- Give it the reason empty, content-mismatch or unknown-format, as the README defines them.
- A file that starts like a valid zip but still fails to load is truncated.
- Keep the exception type for every file that throws.
Leave the encrypted workbooks for the next turn, and add no password handling yet.
Rerun --convert, then compare out/convert-results.csv with Expected/manifest.csv.
List every row whose status or reason disagrees.
What does IronXL do with a password-protected workbook?
Two kinds of protection turn up in an inbox, and IronXL treats them differently.
An encrypted workbook throws ArgumentNullException when loaded with no password and SecurityException with a wrong one, while ProtectSheet only locks editing. The inbox's sheet-protected targets workbook loaded and converted with no password at all. The AI coding assistants guide compares the two kinds in full, and the password guide notes that the file format offers no way to test for protection without trying to open the file.
Handle the two encrypted inbox workbooks.
1. Load each OLE2 .xlsx with its password from config/known-passwords.csv.
2. Give a file with no listed password the status Protected and the reason no-password.
3. Try one wrong password against the payroll file and record the exception type it raises.
4. Rerun --convert and confirm out/convert-results.csv agrees with Expected/manifest.csv on every row.
5. Write out/password-scan.csv with one row per file under out/.
6. Say in each row whether that file contains any known password.
Tell me how many rows of out/password-scan.csv found a password.
Does a DataSet round trip give back the same sheet?
A round trip does not give back the same sheet by default. A pipeline that hands sheets to SQL or Entity Framework through ToDataSet and rebuilds workbooks with LoadWorkSheetsFromDataSet should test the trip. OpenAI documents Codex's built-in worker as an execution-focused agent for implementation and fixes, and the /subagents command switches to its thread while it runs.
Have the worker agent add a --roundtrip mode.
For each Converted file it should:
1. Call ToDataSet.
2. Rebuild a workbook with WorkBook.LoadWorkSheetsFromDataSet.
3. Take the values only from that DataSet and never from the source sheet.
4. Save the rebuilt workbook to out/roundtrip/<name>.xlsx and reload it.
5. Compare every cell's text with the source sheet.
6. Write out/roundtrip-results.csv with one row per sheet.
Give each row these columns:
- rows before
- rows after
- cells compared
- cells that differ
- first difference
Fix the round trip until every sheet reports zero differences, and tell me what each fix was for.
Three things changed the data in our calibration over the 28 converted sheets.
- The header row disappears:
ToDataSetwith a true header argument turns the first row into column names, and the rebuild writes no header row back. Every sheet lost its header row, and 5,267 cells differed.ToDataSetwith no argument defaults to false, keeps the header as data, and avoids the loss. - Text that looks like a date becomes a date: The rebuild parses string values the same way assigning
Cell.Valuedoes. The text 2026-09-01 came back as a DateTime, and the Hamburg date 10.09.2026 (10 September) came back as 9 October under en-CA. With the header problem fixed, 364 cells in six sheets still differed for this reason. - Column types are all object: Every column in the exported DataSet had the type System.Object, and whole numbers arrived as Double. Code that reads the table needs to convert values itself.
Assigning each text value again through Cell.StringValue, which stores the text as written, stops the date conversion. The DataSet guide shows both directions of the conversion.
How do you report every file that failed to convert?
The results file shows which files failed, but the people who fix the inbox also need each reason in words, grouped by status.
I need a written reason for every file that didn't convert.
1. Have each --convert run rewrite out/convert-log.jsonl with one JSON object per inbox file.
2. Build out/failures.md from that JSONL alone.
3. Give each status other than Converted a heading, its count, and each file with its reason.
4. Check the counts in failures.md against out/convert-results.csv and report any mismatch.
Give each JSON object these fields:
- file
- status
- reason
- exception type
- message
- milliseconds
How do you debug incorrect conversion output end to end?
Handed nothing but a symptom, Codex can follow a fault across source files, logs and notes from other teams.
Finance says every European total has been about a hundred times too high since 2026-10-01.
The job and its inputs didn't change.
Send the explorer agent to find the cause.
Have the explorer read Jobs/NightlyConvert.cs, Logs/, Ops/hosts.md and Feedback/finance-2026-10-07.md.
In out/nightly-diagnosis.md, do the following:
- show the line of the job that produces the inflated totals
- explain why conv-01 gave the right totals
- propose the smallest fix, with a command that proves it on all three branch files
Don't edit any code in this turn.
Read the diagnosis and agree with it before anything gets fixed. A strong answer quotes the LoadCSV call that relies on the default field conversion, connects the jump to the move from a German-format Windows host to a container whose LANG is en_US.UTF-8, and proposes keeping the field text and parsing prices with the culture the branch files are written in. In our calibration that change produced the ERP totals under both cultures.
How do you turn one-off code into reusable, tested components?
When the results file and the manifest agree on every row, collapse the modes into a single job that other services can invoke.
Have the worker agent merge the conversion modes into one ConversionJob.
- Wire it up through Microsoft.Extensions.DependencyInjection.
- Pass it a ConversionOptions record that holds the inbox folder and the output folder.
- The record also holds the passwords file and the formats to write.
- Keep the byte check, the password handling and convertFieldValues: false.
- Clear out/converted/ at the start of each run.
- Stay under 180 lines and add XML doc comments.
Then run it over Inbox/ into out/final-results.csv.
Confirm every row matches Expected/manifest.csv on status, reason, sheet count and data rows.
A refactor can drop the byte check, the password lookup or the convertFieldValues setting and still compile.
Add an xUnit project, SheetConvert.Tests, whose tests run ConversionJob on real inbox files.
They should check that:
- branch-summary-q3.xlsx yields nine output files named after its sheets
- customers-uk.csv keeps 000123
- orders-lyon-2026-09.csv keeps 4,20 and six columns
- the zero-byte and HTML files are Unreadable with the README reasons
- bonus-pool-2026.xlsx is Protected
- a DataSet round trip of pricing-2026.xlsx leaves no differing cell
Finish with dotnet test and save its results as a TRX file under out/.
Quote the pass and fail counts from that file.
Output bytes make a poor test target, because a saved XLSX can change byte for byte between releases while every value stays the same.
Keep recurring IronXL questions in the repository skill rather than in ~/.codex/prompts/, which OpenAI's custom prompts page marks deprecated.
$ironxl Look through the IronXL API reference for methods that write CSV, TSV, JSON or XML.
List every such method on WorkBook and WorkSheet.
For each one, say whether it writes one file per sheet or a single file.
1. Check each answer in a scratch project outside this repository.
2. Call each method on a two-sheet workbook, or on one of its sheets.
3. Copy only the files the calls produced into out/api-probe/ and list them.
4. Name the line in ConversionJob that decides each output file's name.
5. Confirm SheetConvert still builds.
How do you reuse these prompts on another spreadsheet folder?
Change the paths and the prompts apply to any other folder of spreadsheets. For one end-to-end acceptance test that starts from an empty folder, see the AI coding assistants guide.
What did the prompts produce in a real run?
We sent an earlier wording of every prompt above to Codex in order, in one session, against a sample .NET 9 console app. Its inbox holds 28 files (single-sheet and multi-sheet XLSX, three legacy XLS files from two different writers, comma, semicolon and tab-separated text, a sheet-protected workbook, two encrypted workbooks, and five broken or mislabeled files). A manifest gives the expected status, sheets and data rows of each, and a passwords file lists one of the two encrypted workbooks. After the run we checked the results below with our own Release build, a run of the test suite, and a separate conversion of every inbox file.
- The first converter marked 23 files converted and 5 failed. The final job matched the manifest on all 28 rows, with 22 Converted, 1 Protected and 5 Unreadable.
- After the byte check, and before any password handling, 21 files converted and 7 were Unreadable, and the only 2 rows that disagreed with the manifest were the encrypted pair.
- Text fidelity went from 147 differing fields across five of the nine text files to 0 in all 9. The culture probe read the Lyon UnitPrice as 420 under en-US and 4.2 under de-DE.
- After the screenshot fix, the 9 account numbers that had lost leading zeros came back, and 60 of 60 matched the source.
- In the quickstart, 7 of 8 sample files reported success and the random-bytes file threw FileLoadException. The zero-byte Newcastle file counted as a success, and Lyon had two columns.
- Exit codes from the key guard were 0 with the key set and 2 with it unset. The key reached Codex only through the environment and never appeared in a file or a reply.
- Survey output held 28 rows, flagged 4 mismatches and left 2 files unidentified.
- Payroll converted with its 25 data rows, the bonus pool was the one Protected row, the wrong password raised SecurityException, and none of the 124 files under out/ held a password.
- The worker agent's round trip, which also copied number formats for typed cells, compared 5,714 cells in 28 sheets with 0 differences. Our own round trip also found 0.
- The failure report listed 5 Unreadable files and 1 Protected file, the same as the results file.
- Codex named line 27 of the nightly job and the en-US container. Under de-DE the branch files gave the ERP totals 25632.85, 11926.93 and 14387.26, and under en-US 100 times those. We added the job, its logs, the host notes and the finance note just before that prompt. A second explorer could not start under the same name, so Codex ran the read-only investigation in its main thread.
- The worker from the round trip wrote a 125-line ConversionJob with dependency injection, and the tests passed 2 of 2, also on a clean clone.
- Prompt 16 left 40 files in out/api-probe/, and SheetConvert still built.
- Codex's sandbox could not reach nuget.org, so restore used the local package cache. All 16 Release builds succeeded. A first full run failed. Its round trip copied source cells over the rebuilt sheet, its byte check also added the password step, its refactor left stale files in out/converted/, and its API probe put a second program inside the project, which broke the build. Prompts 9, 11, 14 and 16 were tightened before the run reported here.
Codex's version, model, timing and test machine for this run are in the shared run record.
Troubleshooting
- C# Read CSV Files when a semicolon file lands in one or two columns
- Convert Spreadsheet File Types when a converted file is missing at the path you passed
- Set a Password to Workbook when an encrypted workbook throws on load
- Apply a License Key in IronXL if every row in the results file shows the same licensing failure
Questions?
If you have any questions, reach out to support@ironsoftware.com
Curtis Chau holds a Bachelor’s degree in Computer Science (Carleton University) and specializes in front-end development with expertise in Node.js, TypeScript, JavaScript, and React. Passionate about crafting intuitive and aesthetically pleasing user interfaces, Curtis enjoys working with modern frameworks and creating well-structured, visually appealing manuals.