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
Create the import Structure to match Excel column headers
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.
Add the Upload widget and bind file variables
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
1/* Upload widget properties */2FileContent: ImportFileContent (Local Variable, Binary Data)3FileName: ImportFileName (Local Variable, Text)4Accept: ".xlsx"56/* ButtonImportOnClick */7Start8 --> If: ImportFileContent = NullBinaryData()9 [True] --> Message: "Please select an Excel file" (Warning) --> End10 --> ImportProducts: FileContent=ImportFileContent11 --> EndExpected result: Upload widget is on screen. Clicking Import without selecting a file shows the warning message.
Create the ImportProducts Server Action with ExcelToRecordList
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.
1/* ExcelToRecordList parameters */2File: FileContent3RecordDefinition: ProductImportRow45/* 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.
Validate each row and insert valid records
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.
1/* For Each loop body */2Assign: CurrentRow = ExcelToRecordList.RecordList.Current34If: Length(Trim(CurrentRow.ProductImportRow.SKU)) = 05 [True] --> Assign:6 ErrorCount = ErrorCount + 17 ErrorMessages = ErrorMessages8 + "Row " + IntegerToText(ExcelToRecordList.RecordList.CurrentRowNumber)9 + ": SKU is empty." + NewLine()10 --> Continue1112If: not TextToDecimalValidate(CurrentRow.ProductImportRow.Price)13 [True] --> Assign:14 ErrorCount = ErrorCount + 115 ErrorMessages = ErrorMessages16 + "Row " + IntegerToText(ExcelToRecordList.RecordList.CurrentRowNumber)17 + ": Invalid price '" + CurrentRow.ProductImportRow.Price + "'" + NewLine()18 --> Continue1920CreateOrUpdateProduct:21 Product.SKU = Trim(CurrentRow.ProductImportRow.SKU)22 Product.Name = Trim(CurrentRow.ProductImportRow.Name)23 Product.Price = TextToDecimal(CurrentRow.ProductImportRow.Price)2425Assign: ImportedCount = ImportedCount + 1Expected result: Valid rows are inserted or updated in the Product entity. Invalid rows accumulate error messages without stopping the import.
Display import results to the user
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.
1/* ButtonImportOnClick - result display */2If: ImportProducts.ErrorCount > 03 [True] --> Message:4 IntegerToText(ImportProducts.ImportedCount) + " rows imported. "5 + IntegerToText(ImportProducts.ErrorCount) + " rows had errors:"6 + NewLine() + ImportProducts.ErrorMessages7 (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
1/* ============================================================2 STRUCTURE: ProductImportRow3 Attributes: SKU (Text), Name (Text), Price (Text), CategoryName (Text)4 ============================================================ */56/* ============================================================7 SERVER ACTION: ImportProducts8 Input: FileContent (Binary Data)9 Output: ImportedCount (Integer), ErrorCount (Integer), ErrorMessages (Text)10 ============================================================ */11Start12 --> ExcelToRecordList:13 File: FileContent14 RecordDefinition: ProductImportRow15 --> Assign: ImportedCount=0, ErrorCount=0, ErrorMessages=""16 --> For Each: Row in ExcelToRecordList.RecordList17 |18 +--> Assign: R = ExcelToRecordList.RecordList.Current.ProductImportRow19 |20 +--> If: Length(Trim(R.SKU)) = 021 | [T]--> Assign: ErrorCount+1, ErrorMessages += "Row N: SKU empty" + NewLine()22 | --> Continue23 |24 +--> If: not TextToDecimalValidate(R.Price)25 | [T]--> Assign: ErrorCount+1, ErrorMessages += "Row N: Invalid price" + NewLine()26 | --> Continue27 |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 + 134 --> Assign output parameters35 --> End3637Exception Handler (AllExceptions)38 --> Assign: ErrorMessages = "Import failed: " + ExceptionMessage39 ErrorCount = -140 --> End4142/* ============================================================43 CLIENT ACTION: ButtonImportOnClick44 ============================================================ */45Start46 --> If: ImportFileContent = NullBinaryData()47 [T]--> Message: "Select an Excel file" (Warning) --> End48 --> ImportProducts: FileContent=ImportFileContent49 --> If: ImportProducts.ErrorCount > 050 [T]--> Message: summary with errors (Warning)51 [F]--> Message: success count (Success)52 --> EndCommon 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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation