How to Use IronXL with Cursor

Curtis Chau
Curtis Chau
Updated: October 15, 2026

A budget job that edits its workbook through Excel Interop runs only where Excel is installed. BudgetDesk, the .NET 9 service in this guide, rolls a cost-center budget forward on one finance desk PC that Task Scheduler wakes at month end. Microsoft does not recommend or support automating Office from unattended code, and its guidance names a Task Scheduler job under the SYSTEM account as one such case. Moving the job to a build agent means replacing Interop. Handing that port to an AI agent adds a quieter risk, because Interop and IronXL disagree on details that still compile.

Cursor's editor is based on the VS Code codebase, and three of its features keep a port like this honest. A project rule names the Interop and EPPlus habits to drop, Plan Mode settles the mapping before any file changes, and documentation links in each prompt give the agent IronXL's real API. Each BudgetDesk roll inserts a row and a column, edits values and formulas, sorts lines by variance, copies the top lines to a second sheet, and adds and drops month sheets. IronXL does all of that to .xlsx files on machines without Office.

Quickstart
NuGetInstall with NuGet

PM > Install-Package IronXL.Excel

Install IronXL by running the command above in the NuGet Package Manager Console, or search for the package in the NuGet Package Manager.

With BudgetDesk open in Cursor, paste this into Agent. Its two mentions hand over the Interop class and the workbook that class edits.

@src/BudgetDesk/Workbooks/BudgetBook.cs @TestData/budget-2026.xlsx

BudgetBook drives Excel over COM. I want to see what IronXL makes of the same workbook.
Fetch https://ironsoftware.com/excel/csharp/llms.txt before you write anything.

1. Run dotnet test once and tell me what the failures have in common.
2. Add IronXL.Excel 2026.10.1 to src/BudgetDesk.
3. Add an "inspect <file>" command to Program.cs that loads the workbook with IronXL.
4. Have it print the sheet names in tab order first.
5. Then have it print every Lines cell that holds a formula, with its address, formula text and value.
6. Run inspect on TestData/budget-2026.xlsx and show me the output.

The program takes the key from IRONXL_LICENSE_KEY and assigns it to License.LicenseKey.
Don't print the key's value or list any environment variables.
Leave BudgetBook.cs as it is for now.
Text

On a machine without Excel, five of the six tests stop at new Excel.Application() with COMException 0x80040154, class not registered. The sixth passes because the Watchlist export uses EPPlus 4.5.3.3. On the same machine, the inspect command lists 15 sheets, which are Summary, Lines and Template followed by twelve month sheets from Oct-2025 to Sep-2026. The Lines formulas it prints are a Variance from E2-D2 to E9-D9 on rows 2 to 9 plus three SUM totals in row 10.

Leave the license key out of every prompt. Cursor's agent edits files and runs shell commands, so a key typed into the conversation can end up in code or a commit message. Set the IRONXL_LICENSE_KEY environment variable instead. IronXL does not read that variable by itself, which is why the prompt has the code assign it to License.LicenseKey. The license key guide covers the exception a missing key throws and the config files IronXL reads on its own.



Which Cursor model should you use for IronXL work?

Composer is Cursor's own agentic model. The model picker also offers SpaceXAI's Grok models, which Cursor co-developed, and models from OpenAI, Anthropic, Google and other labs. The test run below used Grok 4.7.

Planning a port means reading every Interop call and every test and deciding what each becomes, which rewards a model that reasons for longer. Building it is mostly mechanical edits and test reruns, where a fast model keeps each loop short. Whichever you choose, name the files, the tests, and the expected numbers in the prompt, because two models can read one request differently.

How do you give Cursor the IronXL documentation?

Put IronXL's documentation link in the request, or keep it in a committed rule so nobody has to paste it. The llms.txt index links the guides and examples. As of October 2026, Cursor's @ menu attaches files, folders, terminals, chats, Git diffs and the browser, so IronXL's documentation reaches the agent as a link.

A port turns on two questions. Which IronXL arguments count from zero, and what has become of behavior that Interop supplied without being asked?

@src/BudgetDesk/Workbooks/BudgetBook.cs

These are questions, so don't change any files.
Answer from https://ironsoftware.com/excel/csharp/llms.txt and the how-to pages it links.
Cite the page URL for every answer.

1. List each Interop call in BudgetBook in one column and the IronXL member that replaces it in a second.
2. Mark every IronXL argument that counts rows, columns or sheets from 0.
3. Which IronXL method replaces Sort with Header set to xlYes?
4. Does any page describe a header option for that method?
Text

IronXL's pages answer the index question directly. The rows and columns guide describes the InsertRow argument as a zero-based index, and the worksheet guide says every position it mentions is zero-based. Neither the sort guide nor the API reference for SortByColumn mentions a header row.

Tips: Treat an answer without a page behind it as unverified until you find the member in the API reference.
Please note: IronXL also publishes a skill file whose frontmatter names it ironxl. Saved as .cursor/skills/ironxl/SKILL.md, it is discovered when Cursor starts, and typing /ironxl in Agent attaches it to one message.

How do you write project rules that encode your IronXL conventions?

Project rules are .mdc files in .cursor/rules. They are committed with the code, so every clone gets the same rules. A plain .md file in that folder is ignored, because it has no frontmatter to say when it applies. The description, globs, and alwaysApply fields decide when the rule joins a request.

This rule sets globs and leaves alwaysApply off, so it attaches whenever the service or its tests are in context and stays out of requests that never touch a workbook. @ironxl-port attaches it by name. Several patterns go on one line, separated by commas. Save it as .cursor/rules/ironxl-port.mdc.

---
description: Port Excel Interop and EPPlus workbook code in BudgetDesk to IronXL
globs: src/BudgetDesk/**/*.cs, tests/BudgetDesk.Tests/**/*.cs
alwaysApply: false
---

# BudgetDesk edits workbooks with IronXL

- Use IronXL only. The NuGet package is IronXL.Excel and the namespace is IronXL.
  Remove Microsoft.Office.Interop.Excel and EPPlus.
  Don't add ClosedXML, NPOI or another spreadsheet package.
- Documentation: https://ironsoftware.com/excel/csharp/llms.txt
- Delete the COM cleanup along with the Interop calls.
  IronXL code has no Marshal.ReleaseComObject, Application.Quit, GC.Collect or DisplayAlerts.
- Set License.LicenseKey from the IRONXL_LICENSE_KEY environment variable.
  Never write a key into code, tests or logs.
- A1 addresses such as "D10" mean the same in both libraries.
  IronXL index arguments start at 0, where Interop and EPPlus Cells[row, col] start at 1.
  Zero-based: InsertRow, InsertColumn, GetRow, GetColumn, GetCellAt, SetActiveTab, RemoveWorkSheet.
- Write dates as a DateTime plus a FormatString.
  IronXL stores a string that reads as a date, such as Oct-2026, as a date.
- Call EvaluateAll before SaveAs.
- Don't invent IronXL members. When unsure, say so and cite the page.
Markdown

The index bullet is there because an off-by-one row writes the new line below the Total row.

Rules apply to Agent requests. According to Cursor's documentation, they do not affect Tab, and user rules do not apply to inline edits. Check suggestions from those features against the rule yourself.

How do you map Excel Interop and EPPlus calls to IronXL?

A rule that only says "use IronXL" makes the model work out what each call becomes. Each IronXL result was measured with IronXL 2026.10.1 on the BudgetDesk workbook, whose Lines sheet holds eight cost centers in rows 2 to 9 above a Total row in row 10.

Interop or EPPlus callExpectedIronXL
Rows[10].Insert(), Cells[row, col]Counting from 1InsertRow(9) inserts above row 10, GetCellAt(0, 0) is A1. Total moved to row 11, SUM(D2:D9) unchanged, Summary!B2 became Lines!D11
Range.Copy of the line aboveFormula follows the new rowF10 kept E9-D9
EPPlus Copy of A1:F4 to Watchlist A3Formulas follow Watchlist rowsWatchlist F4 got E2-D2 and read 0
Range["F2:F9"].Formula = "=E2/Summary!$B$5*12"One formula per rowAll eight cells got the F2 formula and read 128,666.67
Sort with Header set to xlYes over A1:F9Header stays, rows sort by VarianceNo header option. Numbers sorted as text and 11,200 came fourth
The same sort over A2:F9Formulas move with their rowsRight order, formulas became numbers. E2 set to 0 left F2 at 11,200
FindSheet loop over WorksheetsA missing month is skippedGetWorkSheet returns null, RemoveWorkSheet threw ArgumentException "Index -1 is out of range"
Template.Copy(After: lastSheet)New sheet lands lastCopySheet put it 16th of 16, a duplicate name threw ArgumentException

Insertion itself held up. InsertColumn(5) moved Variance to column G, rewrote its total as SUM(G2:G9), and moved Summary!B4 to Lines!G10. Two rows of the table do more damage than the rest, because they change what a cell means. A formula copied with its old references reports another cost center's numbers. A sort that turns formulas into numbers leaves a Variance column that stops following Actual, so next month's actuals never reach it. Both survive a glance at the totals. SortAscending is no shortcut either, since on a block of columns it sorts each column on its own, and in our test row 2 came back as CC-110 in Facilities, owned by Berg.

Add the translations to the rule as three more bullets.

- Range.Copy keeps formula references unchanged.
  After copying cells that hold formulas, write each formula again for the row it landed on.
- Setting Formula on a multi-cell Range puts the same text in every cell.
  Write per-row formulas in a loop.
- SortByColumn has no header option and leaves values where the sorted formulas were.
  Sort from row 2 down, then write each row's formulas again.
Markdown

Why does GetCellAt return null in IronXL?

The rule lists GetCellAt among the zero-based members, which invites a line-for-line port of Cells[row, col]. Interop returns a Range for any address, filled or not, so AddLine writes the new cost center into the row it inserted. IronXL's GetCellAt returns null "if cell is not filled", as its API reference puts it. After InsertRow(9), all six positions in the new row 10 came back null, and GetCellAt(9, 0).Value = "CC-190" threw NullReferenceException.

Not filled here means absent from the sheet XML. In the shipped workbook, B10 and C10 hold no value but carry a style in the Total row, and GetCellAt returned them as empty cells. G2, one column past Variance, and A11, one row below Total, were null.

Indexing by address always returns a cell. ws["A10"] took CC-190 in the new row, and GetCellAt(9, 0) found it afterward. A port that keeps numeric indexes can fall back to the address when GetCellAt finds nothing.

static Cell At(WorkSheet sheet, int row, int column) =>
    sheet.GetCellAt(row, column) ?? sheet[$"{(char)('A' + column)}{row + 1}"].First();
C#

How do you plan and run a migration to IronXL?

A port touches every method, and undoing a wrong one costs more than writing it did. Switch the chat to Plan Mode with Shift+Tab before sending the next prompt. In Plan Mode Agent reads the code, may ask clarifying questions, and writes a plan in place of edits.

@src/BudgetDesk/ @tests/BudgetDesk.Tests/ @ironxl-port

Plan the move to IronXL. Don't write code yet.

Here's the scope:
- BudgetBook moves off Excel Interop.
- WatchlistExport moves off EPPlus.
- The tests stop reading workbooks with EPPlus.
- BudgetBook keeps its public methods, and the six tests keep their names.

Put this in the plan:
1. List each Interop or EPPlus call with its IronXL replacement and the file it's in.
2. Flag every place where a row, column or sheet number changes base.
3. Explain how copied cells, the Forecast column and the sort keep each formula pointing at its own row.
Text

The plan is a Markdown file you can edit, so a wrong mapping can be fixed in the plan itself. When the plan reads right, click Build, or switch back to Agent and send the next prompt.

@ironxl-port

Build the approved plan.

- Remove the Interop and EPPlus package references and the EmbedExcelInteropTypes target.
- In the tests, change only how workbooks are opened and read.
- Leave every assertion in BudgetBookTests as it is.
- If IronXL can't read what an assertion checks, read the saved file's XML and keep the assertion.

When the port is done:
1. Build the solution and run the tests.
2. Run "dotnet run --project src/BudgetDesk -- roll Oct-2026".
3. Print each Lines row of out/budget-Oct-2026.xlsx with its formulas.
4. Tell me how many tests passed before the port and how many pass now.
Text

In our calibration, a straight translation that swapped each call for its nearest IronXL member compiled cleanly and passed two of the six tests. The four failures were the Forecast formulas, the sort, the Watchlist copy, and the new line's Variance, the same rows as the table above. The port that followed the rule passed all six.

Which Cursor feature fits each change?

Pick one by how far the change reaches. Tab handles a predictable next edit, and inline edit handles one selection that one sentence covers. Agent takes a change that needs several files or a build loop.

Finance has started closing cost centers, so BudgetBook needs a way to drop a line. Highlight the whole AddLine method in BudgetBook.cs, press Ctrl+Shift+L to add it to the open chat, and send the request in Agent mode.

Add RemoveLine(string code) directly below the AddLine method I highlighted.
It finds a line by its code and deletes that row from Lines.
If the code isn't there, throw KeyNotFoundException, the same way SetActual does.
Text

How does IronXL update totals when a row is removed?

RemoveLine comes out as a few lines around GetRow(index).RemoveRow(), and for a line in the middle of the sheet IronXL keeps the totals right. Removing CC-130 from row 4 shifted the lines below it up a row, shrank the Total's SUM(D2:D9) to SUM(D2:D8), and moved Summary!B2 to Lines!D9.

The last line is the exception. Removing row 9, the line directly above Total, moved Total up into row 9 while its formula stayed SUM(D2:D9), a range that now includes the Total cell itself. IronXL evaluated it as CIRCULARREF~.

November's close in this guide shuts CC-140, the line the October sort put directly above Total.

@TestData/closures/Nov-2026.csv @TestData/actuals/Nov-2026.csv @src/BudgetDesk/Program.cs

Finance closed CC-140 at the end of November.

Change roll so it calls RemoveLine for each code in TestData/closures/<month>.csv.
Have roll do that before it applies new lines and actuals.
A month with no closures file closes nothing.

Then check the numbers:
1. Run roll Oct-2026.
2. Run roll Nov-2026 with out/budget-Oct-2026.xlsx as its input.
3. Add up Budget, Actual and Variance from the remaining line cells yourself.
4. Compare your sums with the Lines Total row and Summary B2:B4.
5. If they disagree, find the cause in BudgetBook, fix it and roll November again.

Show me the Total row formulas either way.
Text

With RemoveRow alone, the November output in our calibration held =SUM(D2:D10) in row 10, three Summary cells reading CIRCULARREF~, and a roll that reported a budget of 0. Writing the four Total formulas again after the removal produced a budget of 477,750, actual spend of 606,600, and a variance of 128,850, which match the sums of the eight remaining lines. Give the rule its next bullet.

- After RemoveRow, write the Total row's SUM formulas again.
  Removing the line directly above Total leaves a range that includes the Total cell itself.
Markdown

How do you test the migrated IronXL code?

A Cloud Agent can write the regression suite with nobody watching. Add IRONXL_LICENSE_KEY as a secret in the Cloud Agents dashboard before you start one.

@tests/BudgetDesk.Tests/ @TestData/

Add five xUnit tests to BudgetDesk.Tests that pin down the port.
Write each one as a new test, even where an existing test already covers part of it.

1. Sorting keeps the header in row 1 and a formula in every Variance cell.
2. A line added above Total gets formulas that use its own row.
3. Removing the line just above Total leaves SUM formulas that end one row higher.
4. After the October roll, Oct-2026 is the active sheet.
5. The totals stored in the saved file equal the recalculated ones.

IronXL recalculates on load and has no getter for the active tab.
Read the stored totals and the active tab by unzipping the xlsx and parsing its XML.

Run dotnet test when they're written and tell me how many pass.
Text

The instruction to unzip the file follows from how IronXL saves. SaveAs writes the result each formula last computed. In our measurement, raising CC-110's actual to 104,900 and saving without EvaluateAll left 527,300 in the file as the stored Lines actual total and in Summary, while the cells added up to 535,700. Loading that file back with IronXL reported 535,700, because IronXL computes formulas as it reads them. A test that reads through IronXL therefore passes on a file whose stored numbers are out of date, while openpyxl, which reads the stored results, showed 527,300. Reading one total before saving refreshed only that cell, and calling EvaluateAll before the save refreshed every one, which is why the rule asks for it.

IronXL also cannot tell the suite which sheet is active, since SetActiveTab writes the value and no property reads it back. After SetActiveTab(14), a save, and a reload, DefaultWorkSheet still returned Summary, so the October test takes the activeTab attribute from the workbook XML.

Does IronXL need Windows after an Interop migration?

Not once the port is complete. The Interop build of BudgetDesk raised three CA1416 warnings, one for each Marshal.ReleaseComObject call, each saying the call is only supported on Windows. IronXL's Linux guide says no code changes are required to run IronXL on Linux, and the Docker guide covers containers. The calibration for this guide ran on Windows, so run the tests once on the Linux image you deploy to before the desk PC is retired.

What did the prompts produce in a real run?

The seven prompts went to Cursor in order, in one chat, against a copy of the BudgetDesk repository made for this guide. The copy contains the Interop BudgetBook, the EPPlus Watchlist export, six xUnit tests that read results with EPPlus, and the budget workbook with October's actuals and one new line. The rule file and the November closure files were added right before the prompts that use them, and the license key was set only as an environment variable.

  • The suite passed 1 of the 6 tests before the port and all 6 after it, with every one of the 31 original assertion lines still in place.
  • After the port the Release build had no warnings, and IronXL.Excel 2026.10.1 was the only spreadsheet package left in the app and its tests.
  • Before the November fix, the roll stopped with a FormatException as it read CIRCULARREF~ from the Summary budget cell. Once the Total formulas were written again, the stored November totals equaled the corrected calibration figures, and no circular reference was left in the file.
  • October's roll saved a budget of 511,500, actual spend of 590,950 and a variance of 79,450. All 19 of our workbook checks passed. They covered the header in row 1, nine lines sorted by variance, one formula per row in Forecast and Variance, Oct-2026 as the active sheet, and a Watchlist of CC-120, CC-130 and CC-110 whose formulas use their own rows.
  • The approved plan mapped the active sheet to DefaultWorkSheet. The first test run after the port caught that, and Cursor had the tests read the active tab from the saved workbook XML instead.
  • Five new tests brought the suite to 11 of 11. Our mutation run dropped one behavior at a time from copies of the final code. The five were the Total rewrite, the formula rewrites after the sort and in the new line, the evaluation before saving and the active tab call. Each copy failed the test written for it.
  • Every Release build had 0 errors and needed no hand edits.
  • Some steps ran differently from the page. Prompts were sent by hand in the desktop app, and the @ mentions were pasted as text. The plan was approved by sending the next prompt, with Build never clicked, and the Cloud Agent prompt ran locally in Agent mode, so no dashboard secret was set. This was the second attempt, because the November closure prompt never reached the chat in the first.

For the IronXL package, Cursor version, timing and test machine behind these numbers, see the run record.


Troubleshooting


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.

Read More

Ready to Get Started?

Nuget Downloads 2,280,941Version:2026.10just released