Skip to main content
RapidDev - Software Development Agency
outsystems-tutorial

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

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.

What you'll learn

  • Using the Upload widget to capture an Excel file as Binary Data at runtime
  • Calling ExcelToRecordList to parse an .xlsx file into a typed record list
  • Mapping parsed rows to entity records and performing validation per row
  • Inserting parsed records using CreateOrUpdate entity actions in a For Each loop
  • Collecting and displaying per-row import errors to the user
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate9 min read35-45 minOutSystems 11 and ODCMarch 2026RapidDev Engineering Team
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.

Prerequisites

  • 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

typescript
1/* Upload widget properties */
2FileContent: ImportFileContent (Local Variable, Binary Data)
3FileName: ImportFileName (Local Variable, Text)
4Accept: ".xlsx"
5
6/* ButtonImportOnClick */
7Start
8 --> If: ImportFileContent = NullBinaryData()
9 [True] --> Message: "Please select an Excel file" (Warning) --> End
10 --> ImportProducts: FileContent=ImportFileContent
11 --> 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.

typescript
1/* ExcelToRecordList parameters */
2File: FileContent
3RecordDefinition: ProductImportRow
4
5/* Output */
6RecordList: 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.

typescript
1/* For Each loop body */
2Assign: CurrentRow = ExcelToRecordList.RecordList.Current
3
4If: Length(Trim(CurrentRow.ProductImportRow.SKU)) = 0
5 [True] --> Assign:
6 ErrorCount = ErrorCount + 1
7 ErrorMessages = ErrorMessages
8 + "Row " + IntegerToText(ExcelToRecordList.RecordList.CurrentRowNumber)
9 + ": SKU is empty." + NewLine()
10 --> Continue
11
12If: not TextToDecimalValidate(CurrentRow.ProductImportRow.Price)
13 [True] --> Assign:
14 ErrorCount = ErrorCount + 1
15 ErrorMessages = ErrorMessages
16 + "Row " + IntegerToText(ExcelToRecordList.RecordList.CurrentRowNumber)
17 + ": Invalid price '" + CurrentRow.ProductImportRow.Price + "'" + NewLine()
18 --> Continue
19
20CreateOrUpdateProduct:
21 Product.SKU = Trim(CurrentRow.ProductImportRow.SKU)
22 Product.Name = Trim(CurrentRow.ProductImportRow.Name)
23 Product.Price = TextToDecimal(CurrentRow.ProductImportRow.Price)
24
25Assign: 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.

typescript
1/* ButtonImportOnClick - result display */
2If: ImportProducts.ErrorCount > 0
3 [True] --> Message:
4 IntegerToText(ImportProducts.ImportedCount) + " rows imported. "
5 + IntegerToText(ImportProducts.ErrorCount) + " rows had errors:"
6 + NewLine() + ImportProducts.ErrorMessages
7 (MessageType: Warning)
8 [False]--> Message:
9 IntegerToText(ImportProducts.ImportedCount) + " rows imported successfully."
10 (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 working example

ImportExcel_ServerAction.txt
1/* ============================================================
2 STRUCTURE: ProductImportRow
3 Attributes: SKU (Text), Name (Text), Price (Text), CategoryName (Text)
4 ============================================================ */
5
6/* ============================================================
7 SERVER ACTION: ImportProducts
8 Input: FileContent (Binary Data)
9 Output: ImportedCount (Integer), ErrorCount (Integer), ErrorMessages (Text)
10 ============================================================ */
11Start
12 --> ExcelToRecordList:
13 File: FileContent
14 RecordDefinition: ProductImportRow
15 --> Assign: ImportedCount=0, ErrorCount=0, ErrorMessages=""
16 --> For Each: Row in ExcelToRecordList.RecordList
17 |
18 +--> Assign: R = ExcelToRecordList.RecordList.Current.ProductImportRow
19 |
20 +--> If: Length(Trim(R.SKU)) = 0
21 | [T]--> Assign: ErrorCount+1, ErrorMessages += "Row N: SKU empty" + NewLine()
22 | --> Continue
23 |
24 +--> If: not TextToDecimalValidate(R.Price)
25 | [T]--> Assign: ErrorCount+1, ErrorMessages += "Row N: Invalid price" + NewLine()
26 | --> Continue
27 |
28 +--> CreateOrUpdateProduct:
29 | Product.SKU = Trim(R.SKU)
30 | Product.Name = Trim(R.Name)
31 | Product.Price = TextToDecimal(R.Price)
32 |
33 +--> Assign: ImportedCount = ImportedCount + 1
34 --> Assign output parameters
35 --> End
36
37Exception Handler (AllExceptions)
38 --> Assign: ErrorMessages = "Import failed: " + ExceptionMessage
39 ErrorCount = -1
40 --> End
41
42/* ============================================================
43 CLIENT ACTION: ButtonImportOnClick
44 ============================================================ */
45Start
46 --> If: ImportFileContent = NullBinaryData()
47 [T]--> Message: "Select an Excel file" (Warning) --> End
48 --> ImportProducts: FileContent=ImportFileContent
49 --> If: ImportProducts.ErrorCount > 0
50 [T]--> Message: summary with errors (Warning)
51 [F]--> Message: success count (Success)
52 --> End

Common mistakes

Why it's a problem: Excel column header 'Product Name' does not match Structure attribute 'ProductName'

How to avoid: 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.

Why it's a problem: Using Integer or Decimal type in the import Structure instead of Text

How to avoid: 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.

Why it's a problem: Not handling AllExceptions for password-protected or corrupt Excel files

How to avoid: 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.

Why it's a problem: Calling the Import Server Action in a loop or from a Client Action that also navigates

How to avoid: 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.

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I'm building a runtime Excel import in OutSystems 11 Reactive Web. I have a Product entity (SKU Text, Name Text, Price Decimal, CategoryId Category Identifier). Explain how to: (1) create a ProductImportRow Structure with Text attributes, (2) use ExcelToRecordList to parse an uploaded .xlsx file, (3) validate each row (SKU not empty, Price is valid decimal using TextToDecimalValidate), (4) call CreateOrUpdateProduct for valid rows, (5) collect row-level error messages with row numbers. Use OutSystems expression and action flow syntax.

OutSystems Prompt

Create a runtime Excel import for the Product entity in OutSystems. Steps: (1) Structure 'ProductImportRow' with SKU, Name, Price (all Text), CategoryName (Text). (2) Server Action 'ImportProducts' with input FileContent (Binary Data) and outputs ImportedCount (Integer), ErrorCount (Integer), ErrorMessages (Text). (3) ExcelToRecordList call, For Each loop with SKU empty check and TextToDecimalValidate(Price) check, CreateOrUpdateProduct for valid rows, error accumulation with CurrentRowNumber. (4) AllExceptions handler.

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.