# Runtime Excel Import: Upload, Parse, and Store Data in OutSystems

- Tool: OutSystems
- Difficulty: Intermediate
- Time required: 35-45 min
- Compatibility: OutSystems 11 and ODC
- Last updated: March 2026

## TL;DR

Use the Upload widget to capture an .xlsx file as Binary Data, then pass it to ExcelToRecordList (Logic tab → System) to parse it into a typed record list. Map each row to your entity structure in a For Each loop, validate each row, and call CreateOrUpdate<Entity> to store records. Collect errors per row and display them to the user after the import completes.

## Runtime Excel Import in OutSystems

OutSystems provides Bootstrap from Excel for seeding data at design time, but production apps need to let users upload data at runtime. This tutorial covers the complete runtime import pattern: capturing the upload, parsing with ExcelToRecordList, validating each row, bulk-inserting valid records, and reporting which rows failed. The pattern handles the most common enterprise use case — a non-technical user uploads a spreadsheet and expects to see results immediately.

## Before you start

- A Reactive Web App with an import target entity (e.g., Product with SKU, Name, Price, CategoryId)
- An Excel template file matching your entity's structure (column names must match Structure attribute names exactly)
- Understanding of For Each loops and exception handling in Server Actions

## Step-by-step guide

### 1. Create the import Structure to match Excel column headers

ExcelToRecordList maps Excel column headers to Structure attribute names. The column name in the Excel file must exactly match the Structure attribute name (case-sensitive).

Data tab → Structures → right-click → Add Structure. Name it 'ProductImportRow'. Add attributes matching your Excel columns:
- SKU: Text
- Name: Text
- Price: Text (use Text for all numeric columns so you can validate format before converting)
- CategoryName: Text

Using Text for all attributes in the import Structure is intentional — it lets you validate and convert values yourself before saving to the entity, rather than letting parsing fail silently for invalid numbers.

**Expected result:** ProductImportRow Structure appears in Data tab → Structures with four Text attributes matching expected Excel column headers.

### 2. Add the Upload widget and bind file variables

On your Import screen, drag a Form widget from the Toolbox. Inside the Form, add an Upload widget. In the Properties Panel:
- FileContent: create Local Variable 'ImportFileContent' (Binary Data)
- FileName: create Local Variable 'ImportFileName' (Text)
- Accept: '.xlsx'

Add an 'Import' button below the Upload widget. Right-click → Add OnClick Client Action 'ButtonImportOnClick'.

In ButtonImportOnClick:
Start → If: ImportFileContent = NullBinaryData() → [True] Message 'Please select an Excel file' (Warning) → End → [False] ImportProducts (Server Action) → End

```
/* Upload widget properties */
FileContent: ImportFileContent  (Local Variable, Binary Data)
FileName:    ImportFileName     (Local Variable, Text)
Accept:      ".xlsx"

/* ButtonImportOnClick */
Start
  --> If: ImportFileContent = NullBinaryData()
       [True] --> Message: "Please select an Excel file" (Warning) --> End
  --> ImportProducts: FileContent=ImportFileContent
  --> End
```

**Expected result:** Upload widget is on screen. Clicking Import without selecting a file shows the warning message.

### 3. Create the ImportProducts Server Action with ExcelToRecordList

Logic tab → Server Actions → right-click → Add Server Action. Name it 'ImportProducts'. Add:
- Input: FileContent (Binary Data)
- Output: ImportedCount (Integer), ErrorCount (Integer), ErrorMessages (Text)

Action flow:
Start
  → ExcelToRecordList
      File: FileContent
      (output: RecordList of type ProductImportRow List)
  → Assign: ImportedCount = 0, ErrorCount = 0, ErrorMessages = ''
  → For Each Row in ExcelToRecordList.RecordList
       → (validation and insert logic - next step)
  → End

ExcelToRecordList is in Logic tab → System → ExcelUtils. Drag it after Start. Set File = FileContent. The output RecordList type is inferred from the Structure you specify — in the ExcelToRecordList Properties Panel, set the RecordDefinition to ProductImportRow.

```
/* ExcelToRecordList parameters */
File:             FileContent
RecordDefinition: ProductImportRow

/* Output */
RecordList: List of ProductImportRow (access as ExcelToRecordList.RecordList)
```

**Expected result:** ExcelToRecordList parses the uploaded .xlsx file into a typed list of ProductImportRow records, one per data row in Excel.

### 4. Validate each row and insert valid records

Inside the For Each loop, add validation before inserting each record:

For Each Row in ExcelToRecordList.RecordList
  → Assign: CurrentRow = ExcelToRecordList.RecordList.Current
  → If: Length(Trim(CurrentRow.ProductImportRow.SKU)) = 0
       [True] → Assign: ErrorCount = ErrorCount + 1
                         ErrorMessages = ErrorMessages + 'Row ' + IntegerToText(ExcelToRecordList.RecordList.CurrentRowNumber) + ': SKU is empty.' + NewLine()
               → Continue (skip to next iteration)
  → If: not TextToDecimalValidate(CurrentRow.ProductImportRow.Price)
       [True] → Assign: ErrorCount = ErrorCount + 1
                         ErrorMessages = ErrorMessages + 'Row ' + IntegerToText(ExcelToRecordList.RecordList.CurrentRowNumber) + ': Invalid price.' + NewLine()
               → Continue
  → CreateOrUpdateProduct:
       Product.SKU   = CurrentRow.ProductImportRow.SKU
       Product.Name  = CurrentRow.ProductImportRow.Name
       Product.Price = TextToDecimal(CurrentRow.ProductImportRow.Price)
  → Assign: ImportedCount = ImportedCount + 1

Then assign output parameters before End.

```
/* For Each loop body */
Assign: CurrentRow = ExcelToRecordList.RecordList.Current

If: Length(Trim(CurrentRow.ProductImportRow.SKU)) = 0
  [True] --> Assign:
               ErrorCount    = ErrorCount + 1
               ErrorMessages = ErrorMessages
                               + "Row " + IntegerToText(ExcelToRecordList.RecordList.CurrentRowNumber)
                               + ": SKU is empty." + NewLine()
           --> Continue

If: not TextToDecimalValidate(CurrentRow.ProductImportRow.Price)
  [True] --> Assign:
               ErrorCount    = ErrorCount + 1
               ErrorMessages = ErrorMessages
                               + "Row " + IntegerToText(ExcelToRecordList.RecordList.CurrentRowNumber)
                               + ": Invalid price '" + CurrentRow.ProductImportRow.Price + "'" + NewLine()
           --> Continue

CreateOrUpdateProduct:
  Product.SKU   = Trim(CurrentRow.ProductImportRow.SKU)
  Product.Name  = Trim(CurrentRow.ProductImportRow.Name)
  Product.Price = TextToDecimal(CurrentRow.ProductImportRow.Price)

Assign: ImportedCount = ImportedCount + 1
```

**Expected result:** Valid rows are inserted or updated in the Product entity. Invalid rows accumulate error messages without stopping the import.

### 5. Display import results to the user

After ImportProducts returns, show the user a summary. In ButtonImportOnClick, after the Server Action call:

If: ImportProducts.ErrorCount > 0
  [True] → Message: IntegerToText(ImportProducts.ImportedCount) + ' rows imported. ' + IntegerToText(ImportProducts.ErrorCount) + ' rows had errors:' + NewLine() + ImportProducts.ErrorMessages (Warning)
  [False] → Message: IntegerToText(ImportProducts.ImportedCount) + ' rows imported successfully.' (Success)

For large error lists, display them in a dedicated error Popup or write them to an ImportError entity so users can review them later.

```
/* ButtonImportOnClick - result display */
If: ImportProducts.ErrorCount > 0
  [True] --> Message:
               IntegerToText(ImportProducts.ImportedCount) + " rows imported. "
               + IntegerToText(ImportProducts.ErrorCount) + " rows had errors:"
               + NewLine() + ImportProducts.ErrorMessages
             (MessageType: Warning)
  [False]--> Message:
               IntegerToText(ImportProducts.ImportedCount) + " rows imported successfully."
             (MessageType: Success)
```

**Expected result:** After import, the user sees a success count and, if any rows failed, a list of error messages with row numbers indicating what needs to be corrected.

## Complete code example

File: `ImportExcel_ServerAction.txt`

```outsystems-pseudocode
/* ============================================================
   STRUCTURE: ProductImportRow
   Attributes: SKU (Text), Name (Text), Price (Text), CategoryName (Text)
   ============================================================ */

/* ============================================================
   SERVER ACTION: ImportProducts
   Input:  FileContent (Binary Data)
   Output: ImportedCount (Integer), ErrorCount (Integer), ErrorMessages (Text)
   ============================================================ */
Start
  --> ExcelToRecordList:
        File:             FileContent
        RecordDefinition: ProductImportRow
  --> Assign: ImportedCount=0, ErrorCount=0, ErrorMessages=""
  --> For Each: Row in ExcelToRecordList.RecordList
       |
       +--> Assign: R = ExcelToRecordList.RecordList.Current.ProductImportRow
       |
       +--> If: Length(Trim(R.SKU)) = 0
       |     [T]--> Assign: ErrorCount+1, ErrorMessages += "Row N: SKU empty" + NewLine()
       |         --> Continue
       |
       +--> If: not TextToDecimalValidate(R.Price)
       |     [T]--> Assign: ErrorCount+1, ErrorMessages += "Row N: Invalid price" + NewLine()
       |         --> Continue
       |
       +--> CreateOrUpdateProduct:
       |      Product.SKU   = Trim(R.SKU)
       |      Product.Name  = Trim(R.Name)
       |      Product.Price = TextToDecimal(R.Price)
       |
       +--> Assign: ImportedCount = ImportedCount + 1
  --> Assign output parameters
  --> End

Exception Handler (AllExceptions)
  --> Assign: ErrorMessages = "Import failed: " + ExceptionMessage
              ErrorCount    = -1
  --> End

/* ============================================================
   CLIENT ACTION: ButtonImportOnClick
   ============================================================ */
Start
  --> If: ImportFileContent = NullBinaryData()
       [T]--> Message: "Select an Excel file" (Warning) --> End
  --> ImportProducts: FileContent=ImportFileContent
  --> If: ImportProducts.ErrorCount > 0
       [T]--> Message: summary with errors (Warning)
       [F]--> Message: success count (Success)
  --> End
```

## Common mistakes

- **Excel column header 'Product Name' does not match Structure attribute 'ProductName'** — undefined Fix: ExcelToRecordList matching is case-sensitive and space-sensitive. Either rename the Structure attribute to match the header exactly, or remove spaces from the Excel header. Use underscores in Structure names only if your template uses them too.
- **Using Integer or Decimal type in the import Structure instead of Text** — undefined Fix: If a cell contains non-numeric content (e.g., '12.5a' in a Decimal column), ExcelToRecordList uses the default value (0) silently instead of signaling an error. Use Text and call TextToDecimalValidate() before TextToDecimal() to catch these cases.
- **Not handling AllExceptions for password-protected or corrupt Excel files** — undefined Fix: ExcelToRecordList throws an exception for files it cannot parse. Wrap the Server Action in an AllExceptions handler that returns an informative error message rather than letting the caller see a system exception.
- **Calling the Import Server Action in a loop or from a Client Action that also navigates** — undefined Fix: The Import Server Action is a single call — do not put it in a loop. After import completes, the Client Action can navigate or refresh data. Keep the import atomic (one Server Action call processes all rows).

## Best practices

- Use Text type for all numeric/date columns in the import Structure — validate and convert yourself rather than relying on silent parse failures.
- Provide users with a downloadable Excel template file so column headers match exactly. A mis-named header causes silent data loss.
- Track row numbers in error messages using ExcelToRecordList.RecordList.CurrentRowNumber so users know which Excel rows to fix.
- Use CreateOrUpdateProduct (not CreateProduct) for idempotent imports — re-importing the same file updates existing records rather than creating duplicates.
- Add an AllExceptions handler to the Server Action to catch corrupt file errors (not a valid .xlsx, password-protected, etc.).
- For imports over 1,000 rows, use a Timer pattern: store the file in an entity, trigger a Timer to process it asynchronously, and email results when complete.

## Frequently asked questions

### Does ExcelToRecordList support .xls (old Excel format) as well as .xlsx?

ExcelToRecordList supports only .xlsx (Open XML format) in current OutSystems versions. Files saved in the older .xls binary format will cause a parse error. Instruct users to save as .xlsx, or provide a note in the import UI. The Accept filter '.xlsx' on the Upload widget helps guide users.

### How do I import data from a specific Excel sheet when the file has multiple sheets?

ExcelToRecordList reads the first sheet by default. There is no built-in parameter to select a sheet by name. If you need to read a specific sheet, use a Forge component such as Excel Utils Advanced, which provides sheet name selection. Alternatively, instruct users that the importable data must always be on the first sheet.

### Can I preview the parsed data before committing it to the database?

Yes — parse the file in a Server Action that returns the record list as output, display the data in a Table on screen for user review, then have a 'Confirm Import' button that calls a second Server Action to perform the actual database writes. Store the parsed list temporarily in a Session Variable (O11 Traditional) or a Client Variable (as JSON) between the preview and confirm steps.

### What is the maximum number of rows ExcelToRecordList can handle?

There is no documented hard limit, but practical experience shows performance degrades noticeably above 5,000 rows and the server request may timeout above 10,000-20,000 rows depending on column count and server configuration. For large imports, use a Timer-based asynchronous pattern: store the uploaded file in an entity, trigger a Timer with WakeTimer, and process in batches.

---

Source: https://www.rapidevelopers.com/outsystems-tutorial/outsystems-import-excel-runtime
© RapidDev — https://www.rapidevelopers.com/outsystems-tutorial/outsystems-import-excel-runtime
