#8576·skyvern

FileParserBlock: an Excel cell that says N/A arrives as the string "nan"

Author: L4XBCreated Sep 16, 2026Updated Sep 16, 2026
LabelsbugWorkflows

Bug description

An Excel cell that says N/A reaches a workflow as the string "nan".

FileParserBlock._parse_excel_file_sync reads the workbook with pd.read_excel(file_path, engine="calamine"). pandas applies its default missing-value list to that read, and the list contains the literal strings N/A, NA, n/a, NULL, None, NaN and nan. Each of those cells becomes NaN, and _clean_dataframe_for_json then rewrites NaN to the string "nan".

So the value is not merely lost — it is replaced by a different value, and it becomes indistinguishable from a genuinely empty cell.

The CSV path does not do this. _parse_csv_file_sync uses csv.DictReader, which hands back the string "N/A" unchanged. The same workbook exported as CSV and as XLSX produces two different answers for the same cell.

Reproduction

Measured against the same call and engine the block uses (python-calamine):

python
import pandas as pd
# a sheet whose Status column reads: N/A, NULL, NaN, ok, <blank>
pd.read_excel(path, engine="calamine")["Status"].tolist()
# [nan, nan, nan, 'ok', nan]
pd.read_excel(path, engine="calamine", keep_default_na=False)["Status"].tolist()
# ['N/A', 'NULL', 'NaN', 'ok', '']

After _clean_dataframe_for_json, the first three rows arrive at the workflow as {"Status": "nan"}.

Expected behaviour

N/A is how a person writes "not applicable" in a spreadsheet and NULL is what a database export writes, so the cell carries meaning. It should reach the workflow as the text it holds, the way the CSV path already delivers it. A genuinely empty cell should keep arriving as "nan", as it does today.

Suggested fix

Two lines, contained entirely in _parse_excel_file_sync, leaving _clean_dataframe_for_json and its test untouched:

python
df = pd.read_excel(file_path, engine="calamine", keep_default_na=False)
# A blank cell now arrives as "" rather than NaN; hand the cleaner the NaN it
# expects, so an empty cell still becomes "nan".
df = df.replace("", pd.NA)
return self._clean_dataframe_for_json(df)

A free side effect: without the NaN, a blank no longer upcasts its column to float, so a count column with one gap keeps 12 instead of 12.0.

One thing worth a maintainer's eye rather than mine: _clean_dataframe_for_json also rewrites any value equal to the string "NaN" or "NaT" to "nan". Once the read stops swallowing them, a cell that literally says NaN becomes reachable and that line lowercases it. Six of the seven values come back exactly; NaN comes back as nan. Removing those two string comparisons would fix it, but they predate this and I did not want to touch them blind.

Additional context

I have the patch and would normally send it as a PR. I stopped because I could not run your test suite here — importing skyvern.forge.sdk.workflow.models.block pulls in the whole service layer (google, azure, onepassword and more), so I could not exercise FileParserBlock at all, and I did not want to submit a change to a tested class that I had only verified at the pandas level. If you would like the PR anyway, say so and I will open it.

  • Repository at fb89e9e
  • pandas 3.0.5 with python-calamine, matching the engine the block pins
  • macOS