How to Import a Supplier Catalog Spreadsheet from FTP into NetSuite Items on a Schedule

Build a Spojit workflow that runs every night, downloads your supplier's catalog CSV from an FTP drop, normalizes SKUs and prices, and creates or updates the matching item records in your NetSuite item master.

What This Integration Does

Wholesalers and manufacturers usually receive supplier price lists as a CSV that lands in an FTP folder on a fixed cadence. Keying those changes into NetSuite by hand is slow and error prone, and stale costs flow straight into your margins. This workflow turns that file into a hands-off nightly sync: Spojit picks up the latest catalog drop, cleans up the supplier's column names and price formatting, and writes each row to your NetSuite item master so your purchasing and pricing data stays current without anyone touching the spreadsheet.

A Schedule trigger fires the run on a cron expression in your timezone. Spojit downloads the file from the ftp connector, parses it into rows with the csv connector, and a Loop walks each row. For every SKU the workflow asks NetSuite whether the item already exists and what its current price is, a Condition skips rows whose price has not changed, and the remaining rows are written with upsert-record (or update-record when you already hold the internal ID). Re-runs are safe: because the upsert is keyed on the supplier SKU as an external ID, importing the same file twice updates the same records rather than duplicating them, and the price Condition means an unchanged catalog produces almost no writes.

Prerequisites

  • An ftp connection pointing at the server and folder where your supplier drops the catalog file, with read access to that path.
  • A netsuite connection with permission to read and write item records (the item subtype you use, for example inventoryItem or nonInventoryResaleItem).
  • The exact remote path and filename pattern of the catalog drop (for example /in/catalog/supplier-latest.csv).
  • A column map: which CSV columns hold the supplier SKU, description, and unit price, and which NetSuite item fields they correspond to (for example itemId, displayName, cost).
  • A decision on the external ID convention you will use in NetSuite so each supplier SKU maps to exactly one item record across re-runs.

Step 1: Add a Schedule trigger

Open the Workflow Designer and add a Trigger node, then set its type to Schedule. Enter a 5-field Unix cron expression and an IANA timezone. For a nightly run just after midnight in Sydney, use:

0 1 * * *
Australia/Sydney

A single Schedule trigger can hold more than one cron entry, so you can add a second early-morning run if a supplier sometimes drops the file late. The trigger output is { scheduledAt }, which you can reference downstream as {{ scheduledAt }} for logging or filenames.

Step 2: Download the catalog file from FTP

Add a Connector node on the ftp connector in Direct mode and select the download-file tool. Set path to the remote catalog file and leave encoding as utf8 so the CSV comes back as text:

{
  "path": "/in/catalog/supplier-latest.csv",
  "encoding": "utf8"
}

The tool returns { success, data: { path, content, encoding, size } }. The file text is in {{ download_result.data.content }}. If your supplier writes a dated filename each night rather than a stable one, add a Connector node with the list-directory tool first, sort the entries, and pass the newest filename into download-file.

Step 3: Parse and normalize the rows with the CSV connector

Add a Connector node on the csv connector in Direct mode and select to-json (or parse), passing the downloaded text as csv:

{
  "csv": "{{ download_result.data.content }}"
}

This returns parsed rows under data plus the detected columns. To tidy up the supplier's headers and drop columns you do not need, add a second csv node with the transform tool and use its rename option to map supplier headers to consistent names, for example { "Item No": "sku", "List Price": "price", "Description": "description" }. For unit normalization that the CSV tools do not cover (stripping currency symbols from a price, upper-casing or trimming a SKU), add a Transform node and reshape each field, so that by the end of this step every row exposes clean sku, price, and description values.

Step 4: Loop over each catalog row

Add a Loop node in ForEach mode and point it at the parsed rows, for example {{ csv_result.data }}. Inside the loop body each iteration exposes the current row (referenced as {{ row }} using the item variable name you choose). Everything in Steps 5 through 7 lives inside this loop body so it runs once per SKU. Keep the body lean: the fewer AI and write calls per row, the cheaper and faster the nightly run.

Step 5: Look up the existing item and its current price

Inside the loop, add a Connector node on the netsuite connector in Direct mode using run-suiteql to find the item by the supplier SKU and read back its internal ID and current price. Reference the loop row in the query:

{
  "q": "SELECT id, cost FROM item WHERE itemId = '{{ row.sku }}'"
}

If you prefer a record lookup over a query, use get-item with the internal ID once you have it, passing the correct item subtype in recordType (defaults to inventoryItem). A row that returns no match is a brand-new SKU; a row that returns a match gives you both the internal id for a targeted update and the existing price to compare against in the next step.

Step 6: Skip rows whose price has not changed

Add a Condition node that compares the catalog price against the price NetSuite returned in Step 5. Configure the condition so the workflow only continues down the write branch when the values differ, for example when {{ row.price }} does not equal {{ lookup_result.data.items[0].cost }} (use whichever field name your query returns). Rows where the price is unchanged fall through the false branch and do no write at all, which keeps an unchanged catalog from generating thousands of pointless updates. For brand-new SKUs (no lookup match), route them down the write branch as well so they get created.

Step 7: Create or update the NetSuite item record

On the true branch, add a Connector node on the netsuite connector in Direct mode. The simplest path is upsert-record, which creates the record if the external ID is new and updates it otherwise. Set recordType to your item subtype, set externalId to the supplier SKU, and map the cleaned fields in body:

{
  "recordType": "inventoryItem",
  "externalId": "{{ row.sku }}",
  "body": {
    "itemId": "{{ row.sku }}",
    "displayName": "{{ row.description }}",
    "cost": "{{ row.price }}"
  }
}

If you would rather update existing items by their internal ID and only create the truly new ones, use update-record with the id from Step 5 on matched rows and create-record on rows with no match, splitting the two cases with the Condition. Either way, keying on the supplier SKU is what makes nightly re-runs idempotent.

Tips

  • Add a Send Email node after the loop to mail yourself a short summary (rows processed, rows written, rows skipped) so you have a daily record that the import ran.
  • Keep the supplier SKU as the NetSuite external ID end to end. It is the single key that prevents duplicate items and makes upsert-record behave predictably on every re-run.
  • Cast prices to a consistent numeric format in Step 3 before the Condition compares them, so a value like $12.50 and 12.5 are not treated as different and do not trigger a needless write.
  • If you have several supplier feeds, build the per-row logic once and call it from a Subworkflow node so each feed's workflow shares the same import logic.

Common Pitfalls

  • Wrong item subtype. NetSuite splits items into subtypes (inventory, non-inventory, service). If the catalog mixes types, set recordType per row rather than hard-coding inventoryItem, or the write fails.
  • Timezone drift. The Schedule trigger uses the IANA timezone you set, not the server's. If the supplier drops the file at 11pm local time, schedule the run after that in the same timezone or you will import yesterday's file.
  • Header changes. Suppliers rename or reorder columns without warning. Map columns by name in the csv transform step rather than by position, and add a Condition early that fails the run loudly if an expected column is missing.
  • Stale or partial files. If the supplier is still writing the file when the cron fires, you can read a truncated CSV. Schedule the run with a comfortable margin after the expected drop time, or check the file with get-file-info before downloading.

Testing

Before turning the schedule on, validate against a tiny scope. Place a catalog file with three or four rows in the FTP folder, then trigger the workflow by hand from the Run button instead of waiting for the cron. Walk the execution log: confirm the file downloaded, the rows parsed with clean sku and price values, the lookup found existing items, the Condition skipped an unchanged row and let a changed one through, and the write created or updated exactly the records you expected in NetSuite. Run it a second time with the same file to confirm it produces no duplicate items. When that small batch behaves, point the path at the real drop and enable the Schedule trigger. You can also ask Miraxa, the intelligent layer across your automation, "Why did my last run fail?" to investigate any row that errored.

Learn More

Did this answer your question? Thanks for the feedback There was a problem submitting your feedback. Please try again later.