How it works
- Step 1Export from Shopify admin → Products → Export — Pick
Plain CSV filefor re-importable output. TheFor Excel, Numbers, or other spreadsheet programsoption produces UTF-16 that imports fine but is heavier. Exports of all products or stores with >100 variants get emailed to the store owner rather than downloaded inline. - Step 2Fork the workflow blueprint — Click
Fork to my drafts. The from-blueprint route copies the four-tool chain into your private orchestrator drafts (visibilityprivate), wires consecutive nodes via their first compatible CSV out → in port, and snapshots the graph as version 1 so the fork is reproducible. You must be signed in; no paid tier is required to fork. - Step 3Drop your export onto the first node and Run — Open the forked workflow and drop the CSV onto the
csv-cleanernode. The run panel executes the chain in topological order, passing each step's CSV output into the next. Because none of the four tools is an API connector, the whole run happens in your browser — no paired @jadapps/runner needed. - Step 4Fix curly quotes explicitly (don't rely on Step 1) — The orchestrator's csv-cleaner node does not fold smart quotes, so if your titles or descriptions came through Word/Pages, add a find-replace pass for
“→"and”→"(and‘ ’→'), or run the standalone csv-cleaner tool with the smart-quotes toggle ON before re-import. Otherwise the importer can throwIllegal quoting error. - Step 5Review duplicate-Handle flags before merging — Step 3 marks every row whose
Handlerepeats — but for Shopify this is **expected** within a product (variant rows). Look only at rows where the same Handle appears under differentTitle/Vendorvalues; those are the real cross-product conflicts. Variant rows leaveTitleblank. - Step 6Re-import via Shopify admin → Products → Import — Tick
Overwrite any current products that have the same handleonly if you're updating existing products. The default is unticked, which is why many 'my import did nothing' tickets resolve once it's ticked. Watch the email summary Shopify sends for per-row errors.
Reference screenshots

Handle. Variants of the same product share a Handle by design — only the first row carries Title, Body (HTML), etc. A naive dedupe collapses the variants into a single one-variant product on re-import.Source: external link

cdn.shopify.com/s/files/... paths. The URL must **not** include _small, _thumb, or _medium suffixes — those are derived thumbnails and re-import will fail. Step 2 strips them (use a literal find or the standalone tool's regex mode).Source: external link
The real node chain (blueprint → orchestrator)
Exactly what the forked graph contains, in order, and how the orchestrator's node executor treats each config key. "Honoured" = the key reaches the underlying processor at run time; "Dropped" = the node executor doesn't forward it, so the transform doesn't happen during a workflow run even though the blueprint sets it. Nodes silently ignore unknown keys by design.
| # | Tool node | Output port type | Config in blueprint | Honoured at run time? |
|---|---|---|---|---|
| 1 | `csv-cleaner` | csv | trimWhitespace, removeEmptyRows, repairRows, deduplicate:false, normalizeSmartQuotes:true | trim / repair / remove-empty / dedup-off **honoured**; normalizeSmartQuotes **dropped** by the node executor |
| 2 | `csv-find-replace` | csv | find:_small\., replace:., caseSensitive:true, useRegex:true | find / replace / caseSensitive **honoured**; useRegex **dropped** — find runs as a literal string |
| 3 | `csv-duplicate-finder` | csv | column:Handle, caseSensitive:true | **honoured** — marks repeats with an _is_duplicate column, removes nothing |
| 4 | `csv-header-rename` | csv | renames:{} | **honoured** — empty map is a no-op until you supply a name→name JSON map |
Shopify Product CSV column reference
What each column carries and the cleaning rule that applies. Required for create columns must be present when adding new products; Required for update columns are needed even on partial updates. Anything not in this table is silently dropped on re-import.
| Column | Required | What it carries | Cleaning notes |
|---|---|---|---|
Handle | Yes — every row | URL-safe slug; identifies the product. Variants of one product repeat the same Handle. | Trim whitespace (Step 1). **Never dedupe across rows.** Letters, dashes, numbers only; no spaces. |
Title | Yes for create | Display name. Only the first row of each product carries this — variant rows leave it empty. | Don't fill blank Title cells on variant rows. Use it to tell a variant row from a parent row. |
Body (HTML) | No | Product description. Often contains commas and curly quotes that break parsers. | Stray commas cause column-count drift; Step 1's repairRows fixes them. Curly quotes need Step 2 (smart-quote fold isn't wired into the cleaner node). |
Option1/2/3 Name | Yes for variants | Variant axis labels — e.g. Size, Color. First row only. | **Removing these columns deletes existing variants** on re-import. Leave the columns even if blank. |
Option1/2/3 Value | Yes for variants | The variant axis value on each variant row. | **Changing these values deletes the variant ID** — breaks third-party app references. Don't trim them aggressively. |
Variant SKU | No | Stock-keeping unit per variant. | Trim. Unique per variant within a store — safe to dedupe on (see FAQ). |
Variant Inventory Qty | No (single-location only) | Stock count for the default location. | **Silently ignored on multi-location stores.** Use the separate Inventory CSV via Products → Inventory → Export. |
Variant Price | No | Decimal, store currency. | No currency symbol. Shopify rejects $19.99 — strip $ £ € ¥ with Step 2. |
Image Src / Variant Image | No | Full URL to a product- or variant-level image. | Must **not** include _small, _thumb, or _medium suffixes (Step 2 strips them). |
Status | No | active, draft, or archived. | Lowercase. Default on import is active. |
Run / fork / schedule mechanics
How this workflow actually behaves in the JAD orchestrator, grounded in the runtime. The CSV chain has no Pro-gated or runner-only nodes, so the gates that bite media or connector workflows don't apply here.
| Capability | Status for this workflow | Detail |
|---|---|---|
| Fork | Available to any signed-in user | The from-blueprint route copies the pre-wired graph into private drafts and snapshots it as v1. No paid tier required; forking writes a forked audit event. |
| Tier to run | Free | All four tools default to minTier: free. The tier precheck returns the strictest node tier; with no Pro/Pro+Media nodes here it returns Free — nothing is gated. |
| Where it runs | Your browser (local) | These are CSV tools, not connectors (runnerOnly is forced only on connector-category tools). The run panel uses pure browser execution unless a runner-eligible step is present — so the file stays on your machine. |
| Cron schedule | Off by default | The blueprint ships scheduleCron: null, and forks start unscheduled. Cron is a real feature (the schedule_cron column + a cron enqueue source feed the runner's dispatch poller) but you must opt in by setting a schedule. |
| Run history / trace | Persisted | Each run writes a workflow_runs row with a per-node trace (status, durationMs, output size). The Run Center lists the last 50 runs; mutations (fork, run-trigger) also append to workflow_audit_events. |
| Credentials | None needed | No API keys or tokens are involved — there's no connector node. Connector tools store credentials on the paired runner's local vault, but this chain never touches that path. |
Cookbook
Five concrete ways to run this chain — the default fork-and-run path, the curly-quote correction you should know about, the multi-location inventory caveat, the SKU dedupe variant, and the customer/orders re-target. Each recipe lists the node, the config keys it actually honours, and what to watch for. Where the orchestrator node ignores a config key the standalone tool exposes, that is called out explicitly so you don't trust a transform that didn't run.
Fork and run the default chain
ExampleThe happy path: fork the blueprint, drop a Plain-CSV export on node 1, run end to end in the browser. The output of each step becomes the input of the next.
1. Shopify admin → Products → Export → Plain CSV file 2. /workflows/clean-shopify-csv-workflow → Fork to my drafts 3. Open the fork → drag export.csv onto the csv-cleaner node 4. Click Run csv-cleaner → cleaned.csv (trim + repair + drop blanks, dedup OFF) csv-find-replace → find-replaced.csv (strip _small. from image URLs) csv-duplicate-finder → duplicates-marked.csv (+ _is_duplicate column) csv-header-rename → headers-renamed.csv (no-op until you set a map) 5. Download the final CSV → Products → Import
Correct the curly-quote step (it isn't automatic)
ExampleStep 1's blueprint sets normalizeSmartQuotes:true, but the orchestrator csv-cleaner node drops that key. Add explicit find-replace passes — or run the standalone tool — so Word/Pages quotes don't trigger Illegal quoting error.
In node 2 (csv-find-replace), or as extra find-replace nodes: find: “ replace: " find: ” replace: " find: ‘ replace: ' find: ’ replace: ' Or, before forking, run the standalone tool: /tool/csv-cleaner → enable "Normalize smart quotes" → process → feed result in
Strip all three thumbnail suffixes (literal vs regex)
ExampleThe node drops useRegex, so the blueprint's _small\. find runs as a literal and won't match. Use literal finds in the node, or switch to the standalone tool's regex mode to catch all three suffixes in one pass.
In the workflow node (literal mode), one pass per suffix: find: _small. replace: . find: _thumb. replace: . find: _medium. replace: . Standalone /tool/csv-find-replace with Regex mode ON (one pass): find: _(small|thumb|medium)\. replace: .
Add a safe SKU-based dedupe as a final node
ExampleVariant SKU is unique per variant by design, so deduping on it (unlike Handle) won't eat variants. Append csv-deduplicator to catch phantom rows from a bulk edit gone wrong.
Append node 5: csv-deduplicator columns: Variant SKU strategy: last Result: collapses only rows that are byte-identical duplicates (same Handle, same Option values, same SKU) — true phantoms, not legitimate variants (which differ on Option/SKU).
Re-target the chain for a Customers or Orders export
ExampleCustomers and Orders CSVs aren't variant-aware, so the no-dedupe rule relaxes. Flip Step 1's dedup on and drop the image/variant-specific steps.
Customers / Orders cleanup: csv-cleaner: trimWhitespace ON, repairRows ON, dedup ON (exact) csv-find-replace: optional — strip $/£ from money columns (skip csv-find-replace _small step — no images) (skip csv-duplicate-finder Handle review — no Handle column) csv-header-rename: map to your CRM / BI column names Note: Shopify does not accept Orders CSV import natively — this path is for audit / export to another tool, not re-import.
Edge cases and verbatim errors
Step 1 left curly quotes in place and Shopify threw `Illegal quoting error`
Encoding — failExpected. The orchestrator's csv-cleaner node does not forward normalizeSmartQuotes, so a “ ” pasted from Word survives the run and breaks Shopify's field-quoting state machine. Fix it with an explicit Step 2 find-replace (“→", ”→") or by running the standalone csv-cleaner with the smart-quotes toggle ON before importing.
Step 2 made zero replacements and `_small` URLs are still present
Config driftThe node drops useRegex, so the blueprint find _small\. runs as a literal — the backslash never appears in a real URL, so nothing matches. Change the node's find to the literal _small. (no backslash), or use the standalone csv-find-replace with Regex mode ON. Re-run and confirm the replacement count is non-zero.
A cleaning tool deduped on `Handle` and variant rows vanished
Silent data lossThis is the failure the workflow exists to prevent. The blueprint sets Step 1 deduplicate:false, and Step 3 uses `csv-duplicate-finder` (marks, never removes). If you added a dedupe-on-Handle node yourself, remove it — variants share a Handle by design.
`Line is invalid (No details)` on import
Variant conflict — errorShopify's least helpful error. Cause: a variant option-value conflict — you reassigned an existing variant's option values to a different axis position. Fix: delete the variants in admin first, or re-import under a new Handle. No row number is given — binary-search by splitting the CSV in half (the standalone csv-row-limiter helps you slice it).
`Network error: Unexpected token < in JSON at position 0`
Invalid headers — failShopify returned an HTML error page instead of JSON because a column header doesn't match its canonical name (case-sensitive). Excel autocomplete is a common culprit. Use Step 4 (`csv-header-rename`) to map your headers back to Shopify's exact field names.
Multi-location store: `Variant Inventory Qty` had no effect
Silent — column ignoredOn multi-location stores the Variant Inventory Qty column is silently ignored on import. The cleaning chain can't fix this — it's a Shopify behaviour. Use the dedicated Inventory CSV (Products → Inventory → Export/Import) for stock counts instead.
You dropped an empty file (or a non-CSV) on node 1
No input — errorThe csv-cleaner node reads input.text or the file's text; an empty source yields an empty CSV and downstream steps emit zero-row outputs. A non-CSV (e.g. an .xlsx saved with a .csv name) parses as garbage rows. Convert spreadsheets with an Excel-to-CSV tool first, then feed the real CSV in.
Forked graph is missing an edge between two nodes
Skipped stepThe from-blueprint route wires consecutive nodes via their first compatible out→in port and silently skips an edge if the port types don't match. For this all-csv chain every pair connects cleanly, but if you swap in a node that emits JSON (e.g. csv-row-splitter), the next CSV node sees no input and the runner marks it skipped. Re-wire the edge on the canvas.
Workflow needs a Pro/Pro+Media node and the run is blocked
Tier gate — 403-likeNot triggered by this workflow as shipped — all four CSV tools are Free tier. It applies only if you add a gated node (e.g. a media tool, minTier: pro_media). The tier precheck returns the strictest node tier and blocks the run with an upgrade CTA until you upgrade or remove the gated node.
Re-import did nothing — products unchanged
Import option — reviewAlmost always the Overwrite any current products that have the same handle checkbox was left unticked (the default) while updating existing products. The cleaned CSV was valid; Shopify just treated every Handle as 'already exists, skip'. Re-import with the box ticked.
Frequently asked questions
Why are variant rows kept even though they look like duplicates?
That's the Shopify product CSV format. Each variant is a separate row sharing the same Handle — only the first row carries Title, Body (HTML), Vendor. A repeated Handle within a product is correct. The workflow leaves Step 1 deduplicate:false and uses `csv-duplicate-finder` (marks but doesn't delete) so you can review which Handle repeats are intentional vs. genuine cross-product conflicts.
Does Step 1 actually fix curly quotes during a workflow run?
No — and this is the one gotcha to remember. The blueprint sets normalizeSmartQuotes:true, but the orchestrator's csv-cleaner node only forwards trim / repair / remove-empty to the processor; the smart-quote flag is not wired into the node executor, so it's silently ignored at run time. Fold curly quotes with an explicit Step 2 find-replace, or run the standalone csv-cleaner tool with smart-quotes ON before re-import.
Why does the `_small` find sometimes replace nothing?
The orchestrator's csv-find-replace node does not forward the useRegex flag, so the blueprint's regex _small\. runs as a literal string. The literal _small\. (with a backslash) never appears in a real URL, so it matches zero cells. Use a literal find of _small. in the node, or run the standalone csv-find-replace with Regex mode ON to match _small, _thumb, and _medium in one pass.
Can I dedupe on `Variant SKU` instead of Handle?
Yes, safely. Variant SKU is unique per variant by design, so deduping on it won't eat variants the way deduping on Handle would. Append a `csv-deduplicator` node with columns: Variant SKU as a final step if you suspect bulk-edit-induced phantom rows (every cell identical, same Handle, same Option values, same SKU).
How do I tell a 'real' duplicate from a variant row?
Look at the Title cell. Variant rows leave it empty. A row with Handle = black-shirt and Title = Black Shirt is a parent row; a row with Handle = black-shirt and a blank Title is a variant. Two rows where both have Title filled in under the same Handle are the real cross-product conflict — those are what Step 3's _is_duplicate flag should make you investigate.
Does the workflow work on customer and order CSVs too?
Yes, mostly — see the cookbook recipe. Customers and Orders aren't variant-aware, so you can safely flip Step 1's dedup ON and drop the image/Handle-specific steps. Curly-quote and currency-symbol cleanup still apply. Note Shopify doesn't accept Orders CSV import natively, so for orders this is an audit/export-to-another-tool path, not a re-import path.
What exactly does the Fork button do?
It calls the from-blueprint route, which places each blueprint node in a left-to-right chain, wires consecutive nodes via their first compatible CSV port, creates a private workflow owned by you, snapshots it as version 1, and writes a forked audit event tagged with the blueprint slug. You land in the editor with the four nodes pre-wired — drop your CSV on node 1 and run.
Do I need a paid plan to fork or run this?
No. Forking only requires you to be signed in. Running requires no paid tier either: all four CSV tools default to minTier: free, so the tier precheck returns Free and nothing is gated. Paid tiers only matter if you add Pro or Pro+Media nodes (e.g. media tools) to the chain.
Where does my store data go when I run it?
It stays in your browser. These are CSV tools, not API connectors — the orchestrator forces runnerOnly only on connector-category tools, and the run panel uses pure browser execution unless the workflow contains a runner-eligible step (this one doesn't). Your raw Shopify export is processed locally on the page and is never uploaded to a server.
Can I schedule this to run on a cron?
The blueprint ships unscheduled (scheduleCron: null) and forks start unscheduled. Cron is a real orchestrator feature — a schedule_cron value plus a cron enqueue source feed the runner's dispatch poller — but it presupposes a paired runner to do the dispatched work. For a browser-only CSV chain like this, scheduling adds little; the run is interactive (you drop a file and click Run).
Is there a run history or audit trail?
Yes. Every run writes a workflow_runs row with a per-node trace (status, duration, output size) and an input/output summary; the Run Center shows the last 50 runs. Separately, lifecycle mutations — create, update, publish, fork, run-triggered, cancel — append to workflow_audit_events, which the per-workflow audit endpoint returns to the owner.
What happens if one node in the chain errors mid-run?
By default a node's error policy is abort, so the run stops and the failed node is marked error in the trace. You can set a node to skip (downstream branches see no input and are skipped) or retry (up to 3 attempts with linear backoff). Wrapping a node in a try-catch logic node routes the error around it so an error-handler branch can continue. The persisted trace tells you exactly which node failed.
Local-first by design
This workflow executes entirely on your jadapps-runner. API keys, database credentials, and OAuth tokens are stored in an AES-GCM-encrypted vault on your device — they are never uploaded to JAD Apps' servers. The server only stores the workflow graph (the recipe), not the secrets.