Skip to main content

Bulk import

The problem

A user has a CSV (or XLSX) with hundreds of rows and wants to import them into a table — new customers from a trade show, products from a vendor catalogue, contracts from a migration. They don't want to type each one through the edit dialog.

The pattern

Two parts:

  1. A screen action that opens an Import dialog with a file picker.
  2. A Nomaflow job triggered by the upload that parses the file, validates each row, performs upserts, returns a per-row report.

The framework parses the file (CSV / XLSX / TSV); your only job is to declare the mapping between source columns and target columns.

The recipe

1. Schema preparation

If your target table needs an external key to detect duplicates (and decide insert vs update), make sure it exists:

ALTER TABLE customers
ADD COLUMN external_code VARCHAR(64) UNIQUE;

The bulk import will match incoming rows against this column.

2. Define the import connector

Settings → Connectors → customers → + Add query:

-- upsert
INSERT INTO customers (external_code, name, status, country)
VALUES (:external_code, :name, :status, :country)
ON CONFLICT (external_code) DO UPDATE SET
name = EXCLUDED.name,
status = EXCLUDED.status,
country = EXCLUDED.country,
updated_by = :session_user,
updated_at = CURRENT_TIMESTAMP;

Operation: Write. Declare the four parameters.

For Oracle / SQLite, swap ON CONFLICT for the equivalent (MERGE on Oracle, INSERT OR REPLACE on SQLite).

3. Build the import job

Settings → Jobs → + New job:

FieldValue
Namecustomers-bulk-import
App<app>
Scheduleempty (manual trigger only)
Single instance

Two steps:

Step 1 — parse the file

FieldValue
Nameparse
TypePython
Callableliberty.builtin.bulk_import:parse_file (framework helper)
Kwargsfile_url = ${params.file_url}, mapping = { "Customer code": "external_code", "Name": "name", "Country": "country", "Status": "status" }

The helper:

  • Detects CSV / XLSX from the file extension.
  • Parses the headers; matches against the mapping.
  • Returns a list of rows with the target column names.

Step 2 — upsert each row

FieldValue
Nameupsert
TypePython
Callableliberty.builtin.bulk_import:upsert_rows
Kwargsrows = ${steps.parse.rows}, connector = "customers", query = "upsert", chunk_size = 500

The helper:

  • Splits the rows into chunks of 500.
  • Calls the upsert query per row, capturing successes and failures.
  • Returns { "ok": 487, "failed": 13, "failures": [...] }.

The job parameters expose file_url — declared on the Parameters tab.

4. Add the screen action

Settings → Screens → customers → Actions tab → + Custom button at the toolbar level (not the dialog):

FieldValue
Label↑ Import
Iconupload
VariantSecondary
ActionRun job
Jobcustomers-bulk-import
Parameters dialog(shows the file picker before running)

5. Permissions

Grant job:customers-bulk-import to the roles that should be allowed to import. Skip for read-only roles.

See it work

Open the Customers screen, click ↑ Import. The framework shows a dialog with a file picker. Drop a CSV in, click Run. The job's run-detail page opens with a live log tail; on completion you see "487 imported, 13 failed", with the failures listed per row (line number + error message).

Variations

You want…Do this
Validate before importingAdd a step between parse and upsert that runs per-row validation. Return the list of errors; halt the job if any.
Show a preview before commitRun the job in dry_run = true mode; the upsert_rows helper accepts this flag and only logs what it would do. Then a second click triggers the real run.
Roll back on any errorWrap the upserts in a transaction; the helper has a transactional = true mode.
Schedule a daily import from S3Same job, set a cron schedule + replace step 1 with an http step pulling from S3.

What's next