Documents

Import an Excel file into a data table

Upload a workbook on a start form, read its rows with Read Document, and insert them into a data table — with a Loop and a Data step, or in one Extract step.

Northwind's sales team sends a monthly order export as an Excel workbook. This workflow takes the file from a start form, reads the Orders sheet and adds every order to an Orders data table.

The workbook has a title row and a blank row above the table, a formula column and a date column:

ABCDEF
1Northwind orders — October
2
3OrderIDCustomerQtyUnitPriceTotalOrderDate
410248VINET1214=C4*D401/10/2026
510249TOMSP918.6=C5*D502/10/2026
610250HANAR107.7=C6*D603/10/2026
Test run logs: Read Document read 3 rows from 2 sheets of the workbook, then the loop logged each order
Save & Test: Read Document reports the format, sheets and rows; the loop runs once per row.

1. Create the data table

In Data tables, create Orders with the columns order_id (number), customer (text), quantity (number), unit_price (number), total (number) and order_date (date).

2. Add a start form with a file field

Open the workflow's settings (nothing selected on the canvas) and Design start form. Add an Attachments field with the key workbook, allowed types Spreadsheets, at most 1 file, required. See Task forms.

When the workflow runs, {{form.workbook}} is a list with the uploaded file. Read Document uses the first file of a list, so you can pass it as it is.

3. Read the sheet

Add Read Document after the trigger:

Read Document
{
  "type": "doc_read",
  "label": "Read orders",
  "config": {
    "source": "{{form.workbook}}",
    "sheet": "Orders",
    "header_row": "auto",
    "output_var": "orders"
  }
}

The canvas shows sheet Orders → orders. Header row: Detect skips the title and blank rows and uses row 3 as the header; formulas give their computed values and dates become YYYY-MM-DD. After a test run, {{orders_rows}} is:

{{orders_rows}}
[
  { "OrderID": 10248, "Customer": "VINET", "Qty": 12, "UnitPrice": 14, "Total": 168, "OrderDate": "2026-10-01" },
  { "OrderID": 10249, "Customer": "TOMSP", "Qty": 9, "UnitPrice": 18.6, "Total": 167.4, "OrderDate": "2026-10-02" },
  { "OrderID": 10250, "Customer": "HANAR", "Qty": 10, "UnitPrice": 7.7, "Total": 77, "OrderDate": "2026-10-03" }
]

and the step's output in the run is:

json
{ "variable": "orders", "file": "orders-october.xlsx", "format": "xlsx", "sheets": 2, "words": 25, "characters": 155, "rows": 3, "tables": 1, "truncated": false, "text_preview": "Orders OrderID Customer Qty UnitPrice Total OrderDate 10248 VINET 12 14 168 2026-10-01 10249 TOMSP 9 18.6 167.4 2026-10-02 10250 HANAR 10 7.7 77 2026-10-03" }

sheets is the number of sheets in the workbook. {{orders_count}} is 3; {{orders.structure.sheets.0.header_row}} is 3.

If the header is not detected (for example, a header row that contains numbers), set Header row to the row number (3), or to First row after choosing a Range such as A3:F500.

4. Loop over the rows

Add Loop For Each with the rows as its items:

Loop For Each
{ "type": "loop", "label": "Each order", "config": { "items": "{{orders_rows}}", "item_var": "order" } }

Inside the loop, {{order.OrderID}}, {{order.Customer}}… are the current row; the data picker shows them under Current item.

5. Insert each row

In the loop body, add a Data step that inserts into Orders:

Data (insert)
{
  "type": "update_data_table",
  "label": "Save order",
  "config": {
    "table_id": "<orders-table-id>",
    "table_name": "Orders",
    "operation": "insert",
    "column_values": {
      "order_id": "{{order.OrderID}}",
      "customer": "{{order.Customer}}",
      "quantity": "{{order.Qty}}",
      "unit_price": "{{order.UnitPrice}}",
      "total": "{{order.Total}}",
      "order_date": "{{order.OrderDate}}"
    }
  }
}

6. Run it

Run the workflow and upload the workbook. The run shows Read orders with 3 rows, then Each order three times with a Save order step in each iteration; the Orders table has three new rows. Each insert fires the table's row-created workflows, as any insert does.

The loop is the right choice when each row needs more work: a lookup, a Condition (skip zero quantities), a User Task for large orders.

The shortcut: one Extract step

When the rows go into the table as they are, Extract from Document reads and inserts in one step, without a loop:

Extract from Document
{
  "type": "doc_extract",
  "label": "Import orders",
  "config": {
    "source": "{{form.workbook}}",
    "sheet": "Orders",
    "to_table": true,
    "table_id": "<orders-table-id>",
    "table_name": "Orders",
    "column_mapping": [
      { "column": "order_id", "field": "OrderID" },
      { "column": "customer", "field": "Customer" },
      { "column": "quantity", "field": "Qty" },
      { "column": "unit_price", "field": "UnitPrice" },
      { "column": "total", "field": "Total" },
      { "column": "order_date", "field": "OrderDate" }
    ]
  }
}

In the designer: Insert the rows into a data table, choose Orders, then Map a column for each column. Without a mapping, headers that match a column's key or label (ignoring case, spaces and punctuation) are used, so a table whose column labels are OrderID, Customer… needs none. The run shows:

json
{ "variable": "extracted", "file": "orders-october.xlsx", "tables": 1, "rows": 3, "inserted": 3, "table_columns": ["order_id", "customer", "quantity", "unit_price", "total", "order_date"] }

Extract writes at most 5,000 rows per run; the rest are skipped with a warning. Split bigger files, or use the loop.

Variations

  • CSV instead of Excel: the same steps work; the delimiter is detected.
  • Every sheet: leave Sheet empty in Extract and set Rows from to all to import the rows of every sheet one after the other.
  • Check before importing: add Compare Documents in sheet mode against last month's {{orders_rows}}, keyed on OrderID, and only insert added rows.