How to Sync NetSuite Inventory Levels Back to Shopify on a Schedule

Build a Spojit workflow that pulls authoritative on-hand quantities from NetSuite on a cron schedule and pushes them into your Shopify storefront so the two systems never drift apart.

What This Integration Does

When NetSuite is your system of record for stock, your Shopify storefront is only ever a copy of the truth. Warehouse receipts, returns, and manual adjustments all happen in NetSuite, and unless those numbers flow back to Shopify, shoppers see stale availability: overselling sold-out lines or hiding items you actually have. This tutorial keeps Shopify honest by reading the real NetSuite quantity for each item on a recurring schedule and correcting the Shopify count to match.

The run model is a pull-and-reconcile loop. A Schedule trigger fires on a cron expression (for example every 30 minutes during business hours). On each run the workflow queries NetSuite for current inventory, then for every item it compares the NetSuite quantity against what Shopify currently holds and applies only the difference as a delta. The workflow leaves no extra records behind: it just nudges Shopify quantities into agreement. Because each run recomputes the delta from the live Shopify value, re-runs are safe and self-correcting. If a run is skipped or fails, the next run simply picks up the latest NetSuite numbers and reconciles again.

Prerequisites

  • A NetSuite connection in Spojit with read access to item records and the SuiteQL query endpoint (added under Connections -> Add connection -> NetSuite).
  • A Shopify connection with permission to read and write inventory (the adjust-inventory and get-inventory-levels tools need inventory read/write scope).
  • A mapping between each NetSuite item and its Shopify inventoryItemId, plus the Shopify locationId you fulfil from. Keep this mapping where the workflow can reach it (a NetSuite custom field, a Shopify metafield, or a small lookup table you maintain).
  • The IANA timezone your store operates in (for example Australia/Sydney) so the schedule fires at the hours you expect.

Step 1: Start the workflow on a Schedule trigger

Add a Trigger node and set its type to Schedule. Enter a 5-field Unix cron expression and an IANA timezone. To reconcile every 30 minutes between 8am and 6pm on weekdays, use:

0,30 8-18 * * 1-5

Set the timezone to Australia/Sydney (or your own). A Schedule trigger can hold more than one schedule if you want a different cadence overnight. Each run emits {{ scheduledAt }}, which you can stamp into the summary email later. Start with a wider interval while you test, then tighten it once the sync is proven.

Step 2: Pull authoritative quantities from NetSuite

Add a Connector node in Direct mode, choose the NetSuite connector, and select the run-suiteql tool. SuiteQL lets you read exactly the columns you need in one query instead of fetching whole records. Pull each item's internal id, SKU, and on-hand count:

SELECT item.id AS netsuiteId,
       item.itemid AS sku,
       BUILTIN.DF(item.id) AS displayname,
       aggr.quantityonhand AS onhand
FROM item
LEFT JOIN ( SELECT item, SUM(quantityonhand) AS quantityonhand
            FROM inventorybalance
            GROUP BY item ) aggr ON aggr.item = item.id
WHERE item.itemtype = 'InvtPart'
  AND item.isinactive = 'F'
FETCH FIRST 200 ROWS ONLY

Set limit to control page size and use offset if your catalog runs past one page. Bind the output to a variable such as nsItems. If your account customizes inventory differently, run get-record-metadata once against inventoryItem to confirm the field names before you finalize the query.

Step 3: Loop over each NetSuite item

Add a Loop node in ForEach mode and point it at the rows from the previous step (for example {{ nsItems.items }}, matching the shape your query returns). The loop hands you one item at a time as {{ item }}, exposing {{ item.sku }}, {{ item.onhand }}, and {{ item.netsuiteId }}. Everything inside the loop body runs once per item. Keep the body lean: it should resolve the matching Shopify item, work out the delta, and apply it.

Step 4: Resolve the Shopify item and push the correction in a Subworkflow

The per-item reconcile logic is reusable, so factor it out. Build a second workflow (the child) with a Manual trigger that accepts one item, then call it from the loop body with a Subworkflow node. In the Subworkflow node pick the child workflow under Workflow and pass the current row as Input:

{
  "sku": "{{ item.sku }}",
  "targetQty": {{ item.onhand }},
  "inventoryItemId": "{{ item.shopifyInventoryItemId }}",
  "locationId": "{{ item.shopifyLocationId }}"
}

Inside the child, add a Connector node in Direct mode using the Shopify connector and the get-inventory-levels tool, mapping inventoryItemId from the input. This returns the quantity Shopify currently holds at your location. Next add a Transform node that computes the difference between the NetSuite target and the live Shopify count, since Shopify adjusts by delta rather than absolute value:

delta = targetQty - currentShopifyAvailable

The parent loop pauses while each child run completes, and each child appears as its own entry in execution history, which makes per-item troubleshooting easy. If you would rather not build a child workflow, you can place the get-inventory-levels and adjust-inventory nodes directly inside the loop body instead.

Step 5: Apply the delta to Shopify (only when it changed)

Add a Condition node that checks whether the computed delta is non-zero, so you never make a pointless write when the counts already agree. On the true branch, add a Connector node in Direct mode with the Shopify connector and the adjust-inventory tool. Map these fields:

  • inventoryItemId - the Shopify inventory item id from your mapping.
  • locationId - the Shopify location you fulfil from.
  • delta - the signed difference from Step 4 (positive adds stock, negative removes it).
  • reason - set to correction so the change is labelled clearly in Shopify's history.

This is the only write in the whole workflow. Because the delta is recomputed from the live Shopify value every run, the operation is idempotent: re-running after a partial failure converges on the correct number rather than double-counting.

Step 6: Notify yourself with a run summary

After the loop, add a Send Email node so each sync reports what it did. Send Email uses Spojit's built-in mail service, so no connector is required. Set Recipients to your operations inbox, a templated Subject, and a Body that counts how many items were adjusted and stamps the run time from {{ scheduledAt }}. Set If sending fails to Continue anyway so a mail hiccup never rolls back a successful reconcile. Remember that external recipients must be on your org allowlist under Settings -> General -> Email recipients.

Tips

  • Use SuiteQL aggregation (Step 2) to fetch only the columns you need: it is far lighter than reading full item records and keeps each run fast even for large catalogs.
  • Skip no-op writes with the Condition node in Step 5. Most items will not change between runs, so adjusting only the ones with a real delta keeps you well under Shopify's API rate limits.
  • Quantities only ever push one direction here (NetSuite to Shopify). If a storefront edit slips in, the next run silently corrects it back to NetSuite, which is exactly what a system of record should do.
  • Ask Miraxa, the intelligent layer across your automation, to scaffold the canvas with a prompt like "Add a Loop over {{ nsItems.items }} and a Subworkflow node that passes each item to my reconcile workflow," then fine-tune fields in the properties panel.

Common Pitfalls

  • Sending an absolute quantity instead of a delta. The adjust-inventory tool changes stock by delta, not to a target. Always subtract the live Shopify count from the NetSuite target first, or you will pile quantity on top of quantity.
  • Location mismatch. Shopify tracks inventory per location. If locationId does not match the location you read in get-inventory-levels, your delta is computed against the wrong number. Keep both pointing at the same location.
  • Timezone surprises. A cron like 0,30 8-18 * * 1-5 means nothing without the right IANA timezone. Set it explicitly so the sync runs during your trading hours, not in UTC.
  • Pagination gaps. If your catalog exceeds one page, a fixed FETCH FIRST silently drops the rest. Page through with offset or raise the limit so every item is covered each run.

Testing

Before scheduling it for the whole catalog, scope the SuiteQL query in Step 2 to a single known SKU (add a WHERE item.itemid = 'YOUR-SKU' clause). Run the workflow once with the Run button while the Schedule trigger is still disabled, then open the execution log to confirm NetSuite returned the expected on-hand count, the Subworkflow computed the right delta, and adjust-inventory wrote it. Check the Shopify admin to verify the storefront number now matches NetSuite. Once one item reconciles cleanly, widen the query, enable the schedule, and watch the first few scheduled runs in execution history before trusting it unattended.

Learn More

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