Spree 6 AI Product Import: Pack Cost Is Not a Price
The supplier calls it Pack Cost. Your catalog expects a unit price. The two numbers have never been introduced, and the CSV that just landed wants you to do it before lunch.
The file is made up, but you’ve met its relatives: three rows of olive oil under Item Code, Description, Pack, Pack Cost and Web Status. Row one is a 500 ml tin you already sell, now 66.00 for a case of 12. Row two has no item code, as if the supplier got bored halfway across. Row three is a smoked oil you’ve never stocked, which the supplier has kindly marked active, since apparently they know your shop better than you do.
The Spree Commerce 6 announcement says a connected assistant can take a CSV you give it, propose how its columns map to product fields, run the import, put products into collections, catalogs and price lists, and publish them. Its own example could have been written by somebody staring at my olive oil: “import the supplier’s CSV file, add the new products to the autumn collection and the wholesale price list, then publish them.” One message, done. Here’s how that file becomes a catalog change somebody actually looked at. The Spree behavior comes from Spree’s docs. The oil, the prices and the buyers are invented, and I haven’t run this file through a store.
Pack Cost wants to be the price, and it isn’t
The product import template has 45 fixed columns. Price is what a shopper pays, written as a plain number like 12.50. There’s no column for what you paid the supplier, which is the only money this file contains.
The agent’s tools sit under Files in the Spree MCP tools reference: upload_file, create_import, propose_import_mapping, describe_import_mapping and get_import_status. The proposal is the agent’s guess at which column goes where. Show it a supplier column called Pack Cost and a required field called price, both money, both two decimals, and I’d be surprised if it didn’t suggest they get married.
Accept that and the 500 ml tin goes on sale at 66.00, the cost of twelve tins charged for one, to somebody who wanted to dress a salad. The Spree import and export guide describes three stages (upload, map columns, process) and four fields that must be mapped (Slug, SKU, Name and Price). Then the line to pin above the desk: there is no separate preview, so the rows start processing once you start the import. The template’s warnings are about format (12,50 is read as 1250.00, a word in a price column as 0). Nothing says a number twelve times too big gets questioned.
The same guide says nothing is written to your catalog while an import waits at the mapping step, so that’s where you decide about Pack Cost, on purpose. 66.00 over 12 is 5.50 a tin. The tin sold at 9.00 when the case cost 63.00 (5.25 a tin), and you’ve settled on 9.50. That goes in a price column you add yourself, and Pack Cost stays in your own records. Pack has an honest home in units_per_carton, and the tin gets the slug olive-oil-500ml-tin, a word the supplier has never heard.
The other two rows commit smaller crimes.
Three made-up supplier rows, each with one problem, and what a careful operator sends to Spree. Prices are per tin unless the column says Pack Cost. This is an editorial example, not a test of Spree’s import.
Scroll horizontally to read every column.
| Supplier row (hypothetical) | What the file says | The catch | What goes to Spree | Whose decision |
|---|---|---|---|---|
| Row 1: HL-OIL-500 | Olive oil 500 ml tin · Pack 12 · Pack Cost 66.00 · Web Status active | 66.00 is the case cost. A tin costs 5.50. The template has no cost column, and price is what a customer pays. | sku HL-OIL-500 · slug olive-oil-500ml-tin · price 9.50 (was 9.00) · units_per_carton 12 | Merchant sets 9.50 before upload |
| Row 2: no item code | Olive oil 1 L tin · Pack 6 · Pack Cost 57.00 (9.50 a tin) | No product code. SKU is one of the four fields every import must map. | Nothing. Held back and returned to the supplier | Operator, before upload |
| Row 3: HL-SMK-250 | Smoked olive oil 250 ml · Pack 24 · Pack Cost 84.00 (3.50 a tin) · Web Status active | The supplier’s “active” is not the merchant’s approval. | sku HL-SMK-250 · slug smoked-olive-oil-250ml · price 6.50 · units_per_carton 24 · status column left unmapped, so it arrives as a draft | Operator publishes after sign-off |
Row two looks empty rather than wrong. SKU is one of the four required fields, and the import guide matches lines to existing variants by slug and SKU. The docs don’t say what happens to a single row with a blank SKU, and I won’t guess on Spree’s behalf. The row stays out of the upload and goes back to the supplier with a question. If identity across sizes and pack counts is what keeps you up, read how shopping agents pick the right product variant.
Row three is the polite one. Spree’s status column accepts active, so the supplier’s word fits perfectly and means something else: they’re selling it, not that you’ve tasted or photographed it. Leave Web Status unmapped and, per the template, a new product with an empty status starts as a draft. The template doesn’t say what an empty status does to a product being updated, so open the 500 ml tin afterwards and check, which is the template’s own advice.
A detour before the trade buyers turn up. The clip below doesn’t show Spree or any integration. A connected agent asks a bike shop for a repair quote and gets one back: a specific question, a specific number, and nobody mistaking the quote for a repaired bike.
Getting one tin to the trade kitchens at 7.25
A catalog change is easier to check one tin at a time than as a heroic row count. Say you sell to restaurants through a customer group called Trade kitchens at 7.25 a tin, by the case of 12. The announcement’s example says “the wholesale price list”, and in Spree those words can point at two different things.
The price lists documentation says a list applies by market, channel or quantity, that customer group rules aren’t offered, and that a group gets its own prices through a catalog. The Spree catalogs documentation describes a catalog as assigned to a company or customer group, deciding what they see and what they pay. A trade storefront running as its own sales channel suits a price list. The restaurants need a catalog. Tell the agent which, by name.
The announcement says an agent can add products to catalogs or take them out, and the tools reference lists a curate_products write tool among the catalog tools, so check what your connection shows. I found no tool for typing a catalog price, so treat the 7.25 as your job: the catalog’s price grid, or its price import with SKU and Price. The case rule is an order multiple on the catalog’s commercial terms (the docs’ example: a multiple of 24 allows 24, 48 or 72). One case costs a restaurant 12 × 7.25, which is 87.00, against your 66.00. The smoked oil just goes into a collection called Autumn pantry.
Now the trap. A catalog that lists no products covers your whole range, and adding the first one switches it to restricting mode. If Trade kitchens was price-only and the agent adds one tin, the restaurants open the shop and find a single tin of olive oil. Very minimalist. Not what they ordered.
To see what a restaurant actually pays, the catalogs page says to start a draft order for someone in the group, where the line shows Priced by and the catalog’s name. A draft order holds stock while it exists, so delete it once you’ve seen your 7.25, unless you enjoy explaining phantom stockouts.
The trace, small enough to check by eye. Every check in it is a rule you set, not one Spree enforces.
- Supplier row: HL-OIL-500, Pack 12, Pack Cost 66.00.
- Mapped line: sku HL-OIL-500, slug olive-oil-500ml-tin, price 9.50, units_per_carton 12.
- Product: already active, retail price changes from 9.00 to 9.50 when the row processes.
- Trade kitchens catalog: listed, entered price 7.25, order multiple 12.
- Buyer check: draft order shows 7.25 priced by Trade kitchens, then gets deleted.
Read the card before the agent hits publish
The agent gets the approver’s permissions and no more. The Spree MCP permissions page says you cannot hand over more than you have, that permissions are recorded as they stand on the day you approve, and that actions are recorded against the approver. The tools reference adds that a write asks your client to confirm before it runs. Good. It’s also a confirmation prompt, and anybody who has clicked through forty of them knows what the forty-first feels like.
In late October a developer asked on r/shopifyDev how merchants keep agents to the access they need when several staff connect different agents. They’re building a product around the problem, so read it as one person thinking out loud. Shopify and WooCommerce stores have their own setup, covered in the guide to making Shopify and WooCommerce stores ready for AI agents.
Spree handles the permissions. The review is yours, so split the announcement’s one message in two: import, place, check the buyer price and report, then publish once a person has read the card.
One complication. For a product already on sale, the import is the publishing moment. The guide says a changed price replaces the old one, so the tin sells at 9.50 as soon as its row processes, which is why Pack Cost gets settled at the mapping screen. The smoked oil arrives as a draft and can go live with products_activate, though the products guide says Active alone doesn’t put a product in front of customers. It also has to be listed on a sales channel.
The import result is documented in the Admin API, not the MCP reference. Show an import returns the number, status, rows_count, completed_rows_count and failed_rows_count, and list import rows gives each row’s row_number, status and validation_errors. The tools reference doesn’t say what get_import_status hands an agent, so check yours before you build a card around it.
A completion card for the hypothetical job. The top half uses field names from Spree’s documented Admin API import record, with made-up values. The bottom half is the merchant’s own record, not something Spree produces.
Spree import record
- number
- IM1042 (illustrative)
- status
- completed
- original_filename
- supplier-update-november.csv
- rows_count
- 2
- completed_rows_count
- 2
- failed_rows_count
- 0
- Failed rows (row_number, validation_errors)
- None
Merchant’s own record
- Supplier rows received
- 3
- Held back before upload
- 1: supplier row 2, no item code, sent back to the supplier
- Prices changed
- HL-OIL-500 retail 9.00 → 9.50 (case cost 63.00 → 66.00). New: HL-SMK-250 retail 6.50 (case cost 84.00, 3.50 a tin)
- Placement
- HL-OIL-500 in Trade kitchens catalog at 7.25, order multiple 12 (87.00 a case). HL-SMK-250 in Autumn pantry collection
- Buyer price checked
- Draft order for a Trade kitchens buyer shows 7.25, priced by Trade kitchens. Draft order deleted
- Publication state
- HL-OIL-500 already active, status checked after import. HL-SMK-250 draft until sign-off, then active and listed on the online store channel
- Mapping approved
- [operator name], [date and time]
- Publication approved
- [operator name], [date and time]
Zero failed rows, and the job still rejected a record. Spree can only count the rows it was given, so the 1 L tin lives on your half of the card. It’s the same habit as refusing to celebrate a $227 AI order before it has survived refunds.
The announcement lists the audit log under Enterprise Edition. No doc I read marks the MCP server or the import as Enterprise-only, and the Spree MCP quick start says the server ships in the starter app’s Gemfile, but confirm it against your edition. If your sign-off needs to survive an argument with the supplier next spring, decide where it lives before the first file arrives.
Run this monthly and the supplier, the file layout and maybe the agent all change while the job stays put. A sheet comes in, your pricing gets applied, bad rows go back, good ones get placed, and somebody signs the card. That’s a small catalog-production service, and a service you rerun deserves a name people and other agents can find.
Headless Domains pitches .factory for any factory, build system, maker platform or autonomous production service that wants a name saying exactly what it does. Something like catalog-intake.factory (made up, so check availability) gives the team one place to point when somebody asks what file the service takes and who signs the card. The longer piece on .factory names for production systems covers what goes behind one. The name doesn’t run the import, approve a price or grant anybody permissions.
Spree 6 lets an agent carry a supplier file most of the way to a live catalog. The unit price, the buyer group and the moment of publishing are still choices somebody makes on purpose, so write them on a card, sign it, and then let the agent press the button.
Choose a .factory name for your catalog-production service