How to Use IronXL with Google Antigravity

Curtis Chau
Curtis Chau
Updated: October 15, 2026

A workbook upload page can compile, pass its integration tests, and still fail the first supplier who sends a large price list, a password-protected workbook or a renamed CSV file.

Google Antigravity's /browser command hands the page to a Chrome subagent, which chooses each test workbook in the upload form, submits it, and keeps a screenshot of the response. IronXL opens the upload on the server straight from the request stream with WorkBook.Load, with no Office installation involved. This guide builds a supplier price list page with the two. The page shows a preview table, validates every row against written rules, and returns a cleaned XLSX file, and each of those steps is proved with screenshots and a results table.

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 the MVC project open in Antigravity, ask for the upload page.

/plan Build a supplier price list upload at /PriceList in this ASP.NET Core MVC app on .NET 9.

- A supplier picks an .xlsx or .xls file in a multipart form.
- The server opens it with WorkBook.Load straight from IFormFile.OpenReadStream.
- Use IronXL from the IronXL.Excel 2026.10.1 package. The namespace is IronXL.
- The page shows a preview table, a validation summary and a link to a cleaned XLSX download.
- docs/price-list-rules.md defines every rule and message.

Set IronXL.License.LicenseKey once in Program.cs from IRONXL_LICENSE_KEY.
The app reads that variable on its own, so don't print its value or list environment variables.
Keep the key out of every file.

In the Implementation Plan, tell me:

- which request body limit applies before the action runs
- how the page tells a password-protected workbook from a damaged file
- how text cells get written into the cleaned workbook

Then get dotnet build passing with no errors.
Text

Read two parts of that plan before you approve it. Kestrel rejects any request body over 30,000,000 bytes before MVC binds the form, so a size check inside the action never runs for a larger file. A sound plan raises the limit for this one action with [RequestSizeLimit] and [RequestFormLimits], as Microsoft's file upload guide describes, and keeps its own 10 MB check behind it. The attributes override Kestrel's server-wide limit for that request, so the plan does not also need a larger MaxRequestBodySize in Program.cs, which would raise the ceiling for every endpoint. The plan should also separate a workbook that needs a password from a file that is not a workbook at all, because IronXL throws a different exception for each. In non-interactive mode, agy -p proceeds through plan review automatically, as the Antigravity changelog notes, so you review the plan Artifact together with the code changes afterward.

Give the agent the variable name and nothing more, because a pasted key can end up in source, a commit, or a command log. Even the variable's value can leak. In an earlier run, the agent checked that IRONXL_LICENSE_KEY was set with a PowerShell command that printed its value. The key went to the model and into Antigravity's own transcript files under ~/.gemini/antigravity-cli/, although no file in the repository held it.

The license key guide details all locations IronXL checks for a key.



What is Google Antigravity, and how is it different from a chat assistant?

Given a goal, an Antigravity agent produces a plan, carries it out in the editor, the terminal, and a browser, and reports what it did in Artifacts you can review rather than a chat transcript.

Without guidance, an agent building an upload page may pick the first Excel library it finds or guess a package name. A guessed name can resolve to a counterfeit that an attacker published under that name in advance, a practice known as slopsquatting. Use IronXL.Excel for the package and IronXL for the namespace.

The AI coding assistants guide lists the IronXL documentation links to give the agent and includes a single-prompt acceptance test you can run in Antigravity.

What does the agent hand back, and how do you review it?

Before the code, the Implementation Plan

The plan defines the files. Review it to confirm the scope.

During the run, the code diff

The code diff shows how the plan is implemented. Check the attributes on the POST action, the exception types in each catch clause, and every line that writes a cell in the cleaned workbook. Look up each IronXL member the changes use in the API reference before you accept it.

After the run, the Walkthrough

The Walkthrough summarizes the changes and holds the evidence for the results. Each screenshot is saved as an image Artifact that accepts comments. If one shows a wrong value, such as a date printed as a number, comment on that screenshot instead of describing the problem in a new prompt.

Our CLI conversation kept one walkthrough.md from the first prompt to the last and replaced its contents at every task, so the seven-file tables from earlier prompts survived only in the agent's replies. Keep a copy of each table as soon as its prompt finishes.

How do you test the IronXL upload page in a real browser?

Request a results table that only real uploads can fill.

/browser Start PriceListImport with dotnet run as a background task and open /PriceList.
Its http profile serves http://localhost:5289.

For each file listed in TestData/uploads/expected.csv:

1. Choose it in the upload form and submit.
2. Wait for the page to settle, then take a screenshot.
3. If the outcome column says password, type that row's password, pick the file again and resubmit.

In the Walkthrough, give me one row per file with:

- its size in bytes
- the expected outcome
- what the page showed, either its accepted and rejected counts or the message text
- the EffectiveDate in the first preview row, when there is a preview
- pass or fail against its expected.csv row

If a row doesn't match, quote the server log line or error page behind it before you touch the code.
After every fix, start the app fresh and redo the whole seven-file pass.
Stop the app when you're done.
Text

A higher request limit changes the outcome for one file, but a new date parser touches every preview, so only a full pass over all seven files after each fix shows that nothing else broke. Asking for the log line first also means you review a cause before you review an edit.

Please note: Open the screenshot for price-list-oversized.xlsx first. With raised request limits, it shows the page's own 10 MB message.

What causes wrong or missing Excel values?

Each failure below traces back to one cause, and naming it in a follow-up task saves the agent a round of trial and error.

We adapted the controller from IronXL's ASP.NET MVC guide to read an uploaded form file instead of a fixed path, added a 10 MB length check, and uploaded the seven test workbooks through headless Chrome. Every failure below compiled cleanly.

ScreenshotCauseFix to request
"This site can't be reached" for the 32.7 MB fileKestrel's default body limit[RequestSizeLimit] and [RequestFormLimits] above the app's cap, plus its own size message
"Worbook is password protected, but no password was provided. (Parameter 'password')"The page shows ex.Message from ArgumentNullExceptionA password field and your own message
8 rows of semicolon-joined text in two columnsA renamed CSV loads without an exceptionA header row check first
An insufficient memory message for a small fileA truncated workbook throws FileLoadException"Not a readable workbook"
"2026-11-01 12:00:00 AM", or "46327" for an XLS dateReading the raw value or a DataTable, and an XLS date format IronXL does not detectFormattedCellValue, and DateTime.FromOADate for serials

In our probe an empty file, a PNG image and a plain text file also loaded without an exception, so the header check rejects more bad uploads than any catch block does.

How do you set up a project Antigravity can work in?

Begin from an app that already compiles and serves its home page. dotnet new mvc --framework net9.0 creates one. Start it once and copy the http address from Properties/launchSettings.json, since each new project gets its own port number and the /browser prompts must use it. Keep that address in the http://localhost:<port> form, because the browser's allowlist starts out holding only localhost and any other address waits for someone to click Always allow.

Commit the test workbooks to TestData/uploads with an expected.csv that records the outcome for each one, and write the rules into docs/price-list-rules.md. Our set held a price list that needs cleaning, a legacy XLS export, a workbook with a protected sheet, an encrypted workbook, a truncated download, a renamed CSV and a 32.7 MB workbook. Then have the agent check the package and the key.

Tell me which IronXL.Excel version PriceListImport.csproj references.
Quote its PackageReference line, then list every file that mentions a license key.

The key rules are:

- The key comes from IRONXL_LICENSE_KEY at startup and from nowhere else.
- Neither appsettings file has an IronXL.LicenseKey entry.
- A missing variable produces one startup warning that names it and prints no value.

Add that warning if the app doesn't have it yet. Leave everything else as it is.
Text

Name that version in any later prompt that adds or updates packages, so no task quietly upgrades or downgrades IronXL.

Without a key, every upload fails at the load call with a LicensingException that reads "Production License Required". A page that catches every exception and calls the file unreadable would then blame each supplier for a server problem, which is one more reason to catch only the exceptions you expect.

Antigravity loads rules from AGENTS.md, GEMINI.md and .agents/rules/*.md in the project, according to its rules documentation. A file in .agents/rules/ needs YAML frontmatter with a valid trigger, and Google warns that a rule with missing frontmatter or an unrecognized trigger is discarded silently. A glob trigger loads the rule only when the agent works on matching files, which suits rules that concern one controller and one service.

/learn turns a session's corrections into rules or skills. The slash commands page says it writes .antigravity/rules.md, while the rules page does not list that path among the places rules load from, so name the file in the prompt.

/learn Save what this session settled about the upload page as a glob rule.
Write it to .agents/rules/price-list-upload.md.
Give it YAML frontmatter with trigger: glob.
Its globs cover Controllers/**, Services/** and Views/PriceList/**.

The rule should cover:

- the request limits, with the reason for each value
- the exceptions IronXL throws for a missing password, a wrong password and an unreadable file
- how text that starts with = gets written
- how legacy XLS dates are read
- that no upload change is finished until the seven-file /browser check passes for TestData/uploads
Text

Open the new file and check its first lines for the frontmatter and a snake case trigger value such as always_on, never camelCase alwaysOn. Read the globs value as well. The rules page types it as a comma-separated string and does not describe a YAML list.

How do you load an uploaded workbook with IronXL?

In the ASP.NET Core source, IFormFile.OpenReadStream wraps the form section in a ReferenceReadStream, which can seek because the form reader buffers each file, up to 64 KB in memory and the rest in a temporary file. IronXL needs a stream that can seek. We passed every test workbook to WorkBook.Load through a forward-only stream, and all seven threw NotSupportedException with "Specified method is not supported." The upload stream reported that it can seek, and IronXL read every upload from it without a copy.

The 10 MB cap protects memory more than bandwidth. Loading the 32.7 MB test file, 260,001 rows including the header across 6 columns, took 6.6 seconds and peaked at a 732 MB working set in a console probe. IronXL's file size limits page states that IronXL sets no file size or row limit of its own, so the cap belongs to the app. Set it from the largest price list your suppliers send, and keep the 5,000-row limit from the rules as a second check.

Plan the refactor below in an Implementation Plan Artifact, then wait for my review before editing.

Move all the IronXL work out of the controller into Services/PriceListImporter.cs.
Register the service as a singleton.

The controller:

- keeps the request limits and the 10 MB check
- passes the upload stream to the service, with the password or null when the field is empty
- keeps the cleaned bytes for the download link

The service owns the header row check. It catches three IronXL failures and nothing else:

- a missing password, which throws an ArgumentNullException whose ParamName is password
- a wrong password, which throws a System.Security.SecurityException
- a file that can't be read, which throws a System.IO.FileLoadException

An IronSoftware.Exceptions.LicensingException has to reach the error page and the log.
So does any other failure.
Once the build passes, repeat the browser upload check on all seven workbooks.
Text

When the changes arrive, compare the load step with the version below. It compiles against IronXL 2026.10.1, and with the same three catch clauses our calibration page reached the expected outcome on every one of the seven test files.

using IronXL;

public sealed class PriceListImporter
{
    public ImportResult Import(Stream upload, string? password)
    {
        WorkBook workBook;
        try
        {
            workBook = string.IsNullOrEmpty(password)
                ? WorkBook.Load(upload)
                : WorkBook.Load(upload, password);
        }
        catch (ArgumentNullException ex) when (ex.ParamName == "password")
        {
            return ImportResult.AskForPassword("This workbook is password-protected. Enter its password to open it.");
        }
        catch (System.Security.SecurityException)
        {
            return ImportResult.AskForPassword("That password did not open the workbook. Try again.");
        }
        catch (FileLoadException)
        {
            return ImportResult.Failed("This file is not a readable Excel workbook. Upload an .xlsx or .xls file.");
        }

        WorkSheet sheet = workBook.WorkSheets.First();
        return Validate(sheet);
    }
}
C#

ImportResult and Validate stand for the app's own result type and rule checks.

How do you build the preview table?

An agent asked for a preview may pass the loaded workbook to WorkBook.ExportToHtmlString and put the result in the view. IronXL's Excel to HTML example writes a file with the HTML export method and its options class, and the page describes only the five display options. Our probe called the string export on the four test workbooks the page previews. The good price list, the locked-sheet file and the encrypted file opened with its password each threw InvalidOperationException with "Nullable object must have a value." The legacy XLS file exported 12 of its 13 rows, with PL-3012 missing.

Those three files came from openpyxl, whose default cell fill has no patternType attribute, and the HTML converter inside IronXL fails on that fill. After we added patternType="none" to it, the good price list exported without its last row, PL-2007. Every workbook that exported lost its final row, a three-row workbook created by IronXL included, and the legacy dates still appeared as serial numbers such as 46337.

The export does encode cell text, so a description that used an img tag with src=x and an onerror alert came out with the angle brackets escaped. Build the preview in the Razor view from the sheet's cells instead, so every row the validation counts reaches the screen and Razor encodes each value. If the code changes call the string export, ask the agent to replace it.

How do you write text cells to the cleaned workbook?

One row in our test price list has a description that is plain text beginning with =HYPERLINK(. IronXL reads it back as text, exactly as the supplier stored it. Write that string into the cleaned workbook through Value or StringValue and IronXL stores a live HYPERLINK formula instead. Both writes reloaded as formulas, and the saved sheet XML held an <f> element for the cell. The write guide recommends StringValue to force an exact value, and in IronXL 2026.10.1 that holds for text such as +SUM(1,2) or @SUM(1,2) but not for text that starts with =.

A leading apostrophe fixes it. StringValue = "'" + text stored the rest of the string as a shared string with no formula, the way Excel treats a cell typed with an apostrophe, and it read back as the original text. Setting the cell's FormatString to "@", the Text format, before assigning the value also kept the string as text in our probe. Removing the formula after a plain assignment is no fix, because it left the cell empty.

/goal keeps the agent working on a check like this until it passes, with no pauses for confirmation.

/goal Every cleaned workbook the page produces follows docs/price-list-rules.md.
First start the app in the background on http://localhost:5289. Stop it when you finish.

Work through each file in TestData/uploads that expected.csv marks preview or password:

1. Submit it to the /PriceList form, with the password from its expected.csv row if it has one.
2. Take the download link from the response.
3. Save that download as artifacts/cleaned/<file name>.xlsx.
4. Reload it with WorkBook.Load in a small console project under tools/CleanedCheck.

The web project must not compile tools/CleanedCheck.
The console project sets IronXL.License.LicenseKey from IRONXL_LICENSE_KEY.

For every reloaded file, check:

- the two sheet names
- the accepted and rejected counts against expected.csv
- that no cell in either sheet is a formula
- that the PL-2007 description reads back exactly as the supplier typed it

Keep fixing and rechecking until all four files pass. Then put the results in the Walkthrough.
Text

Both of our calibration cleaners reported 35 accepted and 6 rejected rows on the page, so the counts alone could not tell them apart. The one that copied descriptions unchanged saved a download with one HYPERLINK formula in the Prices sheet.

How do you handle password-protected workbooks?

A workbook with only a protected sheet opens without any password, as the worksheet password guide describes, and our locked-sheet test file loaded and validated like any other. Cleaning a workbook in place instead of building a new one has one catch. After we changed a cell in the loaded file and saved it, the <sheetProtection> element was still in the sheet XML, so a cleaned copy made that way stays protected when the supplier opens it in Excel.

A workbook with an open password is encrypted. Loading it without a password throws ArgumentNullException with the parameter name password, and a wrong or empty password throws System.Security.SecurityException with "Invalid password", so the importer above passes the password only when the field holds text. With the right password, WorkBook.Load(stream, password) opened the file, taking the same second argument the workbook password guide passes after a file path.

The loaded workbook still reported itself as encrypted, yet the bytes from ToByteArray opened without any password. The rules for this page say the cleaned file carries no password, so building a fresh workbook with WorkBook.Create keeps that rule visible in the code instead of leaving it to a side effect.

When should you dispatch more than one agent?

One agent can own the automated tests while a second owns the browser evidence.

Agent A: add a PriceListImport.Tests xUnit project that the web project doesn't compile.
Add public partial class Program { } to the web project so the tests can reach it.
Use WebApplicationFactory<Program>, and for each workbook in TestData/uploads:

1. Post it to /PriceList as multipart form data.
2. Assert the outcome expected.csv gives for it, sending that row's password where it has one.
3. Download the cleaned workbook, if there is one.
4. Reload that workbook with WorkBook.Load and assert the counts.
5. Assert that no cell holds a formula.

Compare cell values, never file bytes.

Agent B: once Agent A's tests pass, run the app on http://localhost:5289 and open /PriceList.
Use /browser to upload price-list-oversized.xlsx and price-list-encrypted.xlsx.
Record each browser session.
Attach both recordings and a screenshot of each final page to the Walkthrough.
Text

Agent B is there because the in-memory test server does not apply Kestrel's limit. We posted the 32.7 MB workbook through WebApplicationFactory to the how-to version of the page, which has no request limit attributes, and the test received status 200 with the page's own "larger than 10 MB" message. If a later change drops the action's request limit, the tests keep passing while suppliers get a connection error, and only the browser session shows it.

Please note: The desktop app orchestrates agents synchronously or asynchronously, so Agent A and Agent B can run as separate tasks. The CLI receives the whole prompt in one turn. Accept Agent B's recordings only when they were captured after Agent A's tests passed on the same commit.

What did the prompts produce in a real run?

The seven prompts ran in one Antigravity CLI conversation against a new ASP.NET Core MVC app. The /learn prompt ran sixth, after the refactor and the /goal check.

  • Our headless Chrome pass over the final app matched the expected outcome for 7 of 7 workbooks. The how-to page, adapted for uploads, previewed 4 of the 7, one of them the renamed CSV, and validated none.
  • Every cleaned download reloaded with Prices and Rejected sheets and counts of 35/6, 12/0, 15/0 and 10/0, with no formula in any cell and the PL-2007 description exactly as the supplier typed it.
  • The Release builds of the app, the reload check and the test project each finished with 0 warnings, and the xUnit suite passed 7 of 7.
  • The 32.7 MB upload reached the action and got the app's size message, because the Quickstart prompt had put both limit attributes at 64 MB and had also raised the multipart body limit for every form to 64 MB in Program.cs.
  • In the agent's own first /browser pass, all 7 rows passed with no code change. The agent made those uploads and screenshots with a Puppeteer script it wrote itself, while the Chrome subagent it started uploaded nothing. The screenshots stayed in Antigravity's Artifacts folder instead of the repository.
  • After the refactor, the importer caught ArgumentNullException by parameter name, SecurityException and FileLoadException, and it left LicensingException to the error page.
  • Our scan found the license key in no repository file, git object or Antigravity transcript.
  • Some steps ran differently from the page. The Quickstart prompt already wrote description cells with the Text format, so none of the calibration failures showed up, and the /goal reload passed on its first check. The goal ran to completion in headless agy -p, with a goal-complete marker at the end of its reply. The Quickstart plan and its code arrived in the same turn, while the refactor prompt stopped after its plan until we approved it.
  • /learn wrote no rule file. It produced a learning proposal Artifact with trigger: glob and a globs value written as a YAML list of three patterns, then waited for approval.
  • One agent did both halves of the two-agent prompt, and its recordings came after the 7 of 7 test pass on the same commit. Each test method hard-codes its file's expected values instead of reading expected.csv.

Antigravity's version, model, and timing, plus the test machine, are listed in the hub's 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