Why APPEND FROM TYPE XL8 Fails with Modern Excel — and How to Fix It with COM Objects

If you maintain a Visual FoxPro application that reads Excel files, there is a good chance you have used APPEND FROM (filename) TYPE XL8 at some point. It is clean, fast to write, and it worked reliably for years. Then one day a user upgrades to Microsoft 365, saves a spreadsheet the same way they always have, and your VFP program either silently produces an empty cursor, complains that the sheet is missing, or — worst of all — crashes the entire session outright.

This is not a bug in your code. It is a compatibility break between VFP 9's built-in Excel import driver and the way modern Excel saves .xls files. Here is what is happening and how to fix it for good.


What Is APPEND FROM TYPE XL8?

APPEND FROM is a classic VFP command that reads records from an external file directly into the currently selected table or cursor. The TYPE XL8 parameter tells VFP to use its built-in ISAM (Indexed Sequential Access Method) driver for Excel 97-2003 format .xls files. The optional SHEET clause lets you target a specific worksheet by name.

When it works, the pattern is elegant:

*-- Classic VFP pattern: import data from an Excel spreadsheet
SELECT 0
CREATE CURSOR IMPORTDATA (ROWTYPE C(3), ORDNO C(20), CUSTCD C(6), AMOUNT N(12,2))
COPY STRU TO (lcTempFile)
USE EXCL (lcTempFile) ALIAS IMPORTDATA

*-- This reads the Excel file row by row, maps columns to cursor fields
APPEND FROM (lcExcelFile) TYPE XL8 SHEET "Data"
IF RECCOUNT("IMPORTDATA") < 2
    =MESSAGEBOX("Could not read spreadsheet.")
    RETURN
ENDIF

VFP reads the spreadsheet row by row and populates the cursor, mapping columns by position to cursor fields. No COM overhead, no external dependencies, no extra libraries -- it all runs inside the VFP runtime.


Why It Breaks with Modern Excel

VFP 9 actually includes ISAM drivers for several generations of Excel's binary format. TYPE XLS targets Excel 2.0 -- the oldest format, which has some quirks in how it handles dates. TYPE XL5 targets Excel 5.0. And TYPE XL8 -- the version most VFP developers have used -- targets what Microsoft called Excel 8.0, the format that shipped with Excel 97 and is commonly known as the Excel 97-2003 format. The number in each TYPE keyword reflects the Excel version number, not an arbitrary label.

The XL8 driver expects files in the OLE Compound Document format: a binary container structure that Microsoft introduced in the early 1990s. You can identify a genuine OLE2 binary .xls file by its first four bytes: D0 CF 11 E0.

When Microsoft introduced the .xlsx format with Excel 2007, they also changed how newer versions of Excel generate .xls files. Excel 365 and later versions increasingly save .xls files in a format that the VFP 9 32-bit ISAM driver no longer correctly recognizes -- even though the file extension is still .xls. The internal structure does not match what the aging driver expects.

The result depends on your exact VFP configuration, but you will typically see one of three behaviors:

  • Silent empty cursor -- APPEND FROM completes without an error, but RECCOUNT() returns 0 or 1, and your downstream logic breaks quietly.
  • "Sheet not found" error -- VFP can open the outer file container but cannot locate the named worksheet, even when the tab is clearly visible in Excel.
  • Hard crash -- VFP 9 terminates immediately, even when run from the command window. No error dialog, no recovery.

The crash scenario is the most damaging in a production ERP environment. It drops the user out of the system with no warning and no error message they can report.


Confirming the File Format

If you want to verify whether a specific .xls file is the problem, you can check its actual binary format from PowerShell without opening Excel:

$bytes = [System.IO.File]::ReadAllBytes("C:\path\to\file.xls") | Select-Object -First 4
$bytes | ForEach-Object { "{0:X2}" -f $_ }
  • D0 CF 11 E0 -- Genuine OLE2 binary .xls. The VFP XL8 driver can read this.
  • 50 4B 03 04 -- ZIP header. This is an OOXML-format file with an .xls extension. VFP will fail on this file.

This problem is not limited to Microsoft 365. When Microsoft introduced the OOXML format with Excel 2007, the "Save As Excel 97-2003 Workbook (.xls)" option began producing files that no longer reliably conformed to the OLE2 binary structure the VFP XL8 driver expects. The incompatibility has grown more pronounced with each successive release -- but if your users are on Excel 2007 or later, you may already be seeing this. You cannot reliably force genuine OLE2 output from any modern Excel version, and the FORMAT := 56 workaround in Excel COM automation does not consistently help. The correct fix is to stop relying on the VFP ISAM driver entirely.


The Fix: Excel COM Automation

The reliable, version-independent solution is to replace APPEND FROM TYPE XL8 with Excel's own COM automation interface. Instead of asking VFP's built-in driver to parse the file, you ask Excel itself to open the workbook -- which always works, because Excel knows its own file formats.

VFP 9 has full COM support. The same Excel COM objects already used throughout VFP applications for writing spreadsheets work equally well for reading them.

Note: The examples below are adapted from real production code. They reference application-specific elements -- a custom message function (XTCMESS()), a named dialog window (DIALOG2), and a cursor structure (IMPORTDATA) that belong to that particular application. Treat these as illustrations of the pattern and the key techniques, not as copy-paste-ready code. You will need to adapt the variable names, cursor structure, and error handling to match your own application.

Before -- Broken with Excel 365

*-- VFP 9 original code -- crashes or silently fails with Excel 365 output
APPEND FROM (lcExcelFile) TYPE XL8 SHEET "Data"
IF RECCOUNT("IMPORTDATA") < 7
    DEACTIVATE WINDOW DIALOG2
    USE IN IMPORTDATA
    =XTCMESS("Selected spreadsheet had less than 7 rows - Update CANCELED...", "SOK")
    RETURN
ENDIF

After -- Works with Any Excel Version

*-- Step 1: Open the workbook via COM -- compatible with all Excel versions
LOCAL loXL, loWB, loWS, lnLastRow, lnRow, loData
LOCAL lcRowtype, lcOrdno, lcCustcd

loXL = CREATEOBJECT("Excel.Application")
loXL.Visible       = .F.
loXL.DisplayAlerts = .F.

TRY
    loWB = loXL.Workbooks.Open(lcExcelFile)
CATCH TO loErr
    loXL.Quit
    loXL = .NULL.
    DEACTIVATE WINDOW DIALOG2
    USE IN IMPORTDATA
    =XTCMESS("Could not open spreadsheet: " + loErr.Message, "SOK")
    RETURN
ENDTRY

*-- Step 2: Find the target sheet by name (case-insensitive)
loWS = .NULL.
LOCAL lnSht
FOR lnSht = 1 TO loWB.Sheets.Count
    IF UPPER(ALLTRIM(loWB.Sheets(lnSht).Name)) = "DATA"
        loWS = loWB.Sheets(lnSht)
        EXIT
    ENDIF
ENDFOR

IF ISNULL(loWS)
    loWB.Close(.F.)
    loXL.Quit
    loXL = .NULL.
    DEACTIVATE WINDOW DIALOG2
    USE IN IMPORTDATA
    =XTCMESS("Data sheet not found in spreadsheet - Update CANCELED...", "SOK")
    RETURN
ENDIF

*-- Step 3: Validate row count
lnLastRow = loWS.UsedRange.Rows.Count
IF lnLastRow < 7
    loWB.Close(.F.)
    loXL.Quit
    loXL = .NULL.
    DEACTIVATE WINDOW DIALOG2
    USE IN IMPORTDATA
    =XTCMESS("Selected spreadsheet had less than 7 rows - Update CANCELED...", "SOK")
    RETURN
ENDIF

*-- Step 4: Read ALL data in one round-trip for speed (cols A-D = 1-4)
*-- This reads the entire range as a 2D array -- much faster than cell-by-cell
loData = loWS.Range(loWS.Cells(1,1), loWS.Cells(lnLastRow, 4)).Value

*-- Step 5: Close Excel immediately -- no need to keep it open
loWB.Close(.F.)
loXL.Quit
loXL = .NULL.

*-- Step 6: Loop through the array and populate the cursor
SELECT IMPORTDATA
FOR lnRow = 1 TO lnLastRow
    lcRowtype = SafeStr(loData(lnRow, 1))
    lcOrdno   = SafeStr(loData(lnRow, 2))
    lcCustcd  = SafeStr(loData(lnRow, 3))
    INSERT INTO IMPORTDATA (ROWTYPE, ORDNO, CUSTCD, AMOUNT) VALUES ( ;
        lcRowtype, lcOrdno, lcCustcd, ;
        IIF(VARTYPE(loData(lnRow,4))='N', loData(lnRow,4), 0) ;
    )
ENDFOR

Helper Function: SafeStr

Excel COM returns .NULL. for empty cells and may return a numeric type (N) for cells that look like numbers. This small helper converts any cell value to a clean, trimmed string:

*-- Safely convert a COM cell value to a trimmed string
FUNCTION SafeStr(tuVal)
    IF ISNULL(tuVal)
        RETURN ''
    ENDIF
    DO CASE
    CASE VARTYPE(tuVal) = 'C'
        RETURN ALLTRIM(tuVal)
    CASE VARTYPE(tuVal) = 'N'
        RETURN ALLTRIM(STR(tuVal, 15, 0))
    OTHERWISE
        RETURN ALLTRIM(TRANSFORM(tuVal))
    ENDCASE
ENDFUNC

Key Things to Watch For

Read the range in one call, not cell by cell. The most common performance mistake with Excel COM is accessing loWS.Cells(row, col).Value inside a nested loop. For a 500-row spreadsheet that would be thousands of individual COM calls. The loWS.Range(...).Value call reads the entire block into a VFP 2D array in a single round-trip, then you access loData(row, col) entirely from memory. The speed difference is dramatic.

Always check for .NULL. Excel COM returns .NULL. for empty cells. If you skip the ISNULL() check and try to use that value in a REPLACE or INSERT, VFP will throw a type mismatch error. The SafeStr helper handles this cleanly.

Numeric cells need type-aware conversion. Pack numbers, order numbers, or any column where Excel "sees" the content as a number will come back as type N rather than C. Using TRANSFORM() directly on a numeric double may give you decimal places you did not want (e.g., "12345.00000"). The STR(val, 15, 0) pattern truncates cleanly.

Close the COM objects before releasing them. loWB.Close(.F.) and loXL.Quit() must be called explicitly. Releasing the object reference without quitting first leaves an orphaned EXCEL.EXE process in Task Manager. In a busy environment where this code runs many times a day, those phantom processes accumulate.

Wrap Workbooks.Open in TRY/CATCH. File-not-found, network disconnects, and password-protected workbooks all throw at that line. Without the catch block, your VFP program will crash at Open rather than inside APPEND FROM -- same outcome, different line.

Wrap the replacement in a single import routine, not inline code. If your codebase has multiple places that call APPEND FROM TYPE XL8, resist the temptation to replace each one individually with the full COM block. Instead, write one reusable function -- something like ImportFromExcel(lcFile, lcSheet, lcCursorName) -- and call it everywhere. This keeps the COM logic in one place, makes future changes much simpler, and keeps your calling code clean. It also means that if you later switch to a non-COM approach (see the next section), you only change one function.


Other Approaches: Reading Excel Without COM

CREATEOBJECT("Excel.Application") requires a licensed, installed copy of Excel on the machine running your VFP program. In a typical ERP workstation environment that is usually a safe assumption -- but it is not universal. Servers, shared workstations, and Microsoft 365 plans that do not include the full desktop Excel application are all scenarios where COM is not available.

The VFP community has produced several alternatives worth evaluating:

  • Greg Green's XLSX Workbook Class -- A pure-VFP class that reads .xlsx files without COM or any external dependency. Source and documentation: github.com/ggreen86/XLSX-Workbook-Class

  • Vilhelm-Ion Praisach's APPENDFROMXLSX -- A VFP procedure that replicates the familiar APPEND FROM pattern for .xlsx files. Details and download: praisachion.blogspot.com

  • Cetin Basoz's ADO-based approach -- Uses ADO (ActiveX Data Objects), a Windows-native technology, to read Excel files without launching Excel itself. No Excel installation required. Example: stackoverflow.com

None of these is a drop-in replacement for APPEND FROM -- each has its own API and trade-offs -- but they give you solid options when COM is not available or not desirable.


FAQ

Does this fix require Excel to be installed on the machine? Yes. CREATEOBJECT("Excel.Application") requires a licensed copy of Excel on the same machine running the VFP program. If Excel is not available -- on a server, a shared workstation, or under a Microsoft 365 plan that does not include the full desktop application -- see the Other Approaches section above for community-developed alternatives that do not depend on an Excel installation.

Can I still use APPEND FROM TYPE XL8 for files I know are in the old format? Yes, if you can guarantee the file came from Excel 2003 or earlier and was never re-saved by a newer version. In practice, that guarantee is hard to maintain as users upgrade. The COM approach is safer for any file you did not generate yourself, since it handles both old and new formats transparently.

What if I do not know the sheet name ahead of time? The FOR lnSht = 1 TO loWB.Sheets.Count loop lets you iterate all sheets and match by name. You can also use loWB.Sheets(1) to simply grab the first sheet regardless of name -- useful when the workbook is always single-sheet.

Is the COM approach meaningfully slower than APPEND FROM? With the bulk range read (loWS.Range(...).Value), the practical difference is under a second for the typical ERP spreadsheet sizes of a few hundred to a few thousand rows. Cell-by-cell reading would be noticeably slow, but the range read approach is nearly equivalent to the old ISAM driver for any spreadsheet you would realistically process interactively.

Will this work with .xlsx files as well as .xls? Yes. loXL.Workbooks.Open() handles both formats transparently. Once you are using COM to read, the file format is Excel's problem to sort out -- not yours.

We have dozens of places in the codebase that use APPEND FROM TYPE XL8. Do we need to replace all of them? Only the ones reading files that users are generating or saving with modern Excel. Files that VFP itself creates via COM (using oWorkbook.SaveAs(filename, 56)) are less likely to trigger the problem, but even those are not safe if a user later opens and resaves the file in Excel 365. The safest approach is to replace all occurrences over time and add the COM pattern to your internal development standard going forward.


Migrating away from APPEND FROM TYPE XL8 is one of those maintenance tasks that feels optional until the day it is not. If your VFP ERP application is still in active use -- as many are -- and your users are on Microsoft 365, this is a breaking change waiting to happen. The COM approach is not significantly more complex, and it is the same pattern you are already using everywhere in VFP code that writes to Excel. The fix fits naturally into the existing codebase.

If you are running a legacy FoxPro or VFP-based ERP system and need help assessing or updating your Excel integration code -- or if you are evaluating a modern ERP upgrade path -- contact PC Methods. We have been working with these systems for over 37 years.

Peter Heinicke

Peter Heinicke

Chicago area ERP consultant and Managed Service Provider with over 45 years of experience in Sage 300, Sage Pro, Quickbooks ERP and other systems


Recent posts

Related posts

Improper methods of fixed assets reporting can lead to losses

Spreadsheets weren't designed for the potentially complex job of tracking and managing fixed...

Continue reading

Sage Pro 2010 is Here

Continue reading

What's new for you in Sage Pro ERP 2011?

Sage Pro 2011 is officially available now. There are some very convenient enhancements in Sage Pro...

Continue reading