How to Process Forwarded Supplier Price-Change Emails into MarketMan Catalog Updates
Build a Spojit workflow that catches a supplier price-list email at a mailhook address, fetches the attached CSV, uses an Agent-mode Connector node to map each row to your MarketMan vendor catalog, and updates pricing after a human approves the change set.
What This Integration Does
Suppliers send price changes as a spreadsheet attached to an email, and someone on your team retypes those numbers into MarketMan by hand. That is slow and error prone, and a single mistyped unit cost flows straight into your food-cost reporting. This workflow turns the forwarded email into a structured change set: it reads the attached CSV, matches each supplier line to the right vendor product in MarketMan, and stages the new prices for a human to review before anything is written back.
The run starts when mail lands on a Spojit mailhook address that you point your supplier (or an inbox forwarding rule) at. An Attachment node pulls the CSV bytes, the csv connector parses the rows, and a Connector node in Agent mode reasons over your live MarketMan vendor products to build a row-by-row mapping. A Human node pauses the run until an approver signs off, and only then does the workflow call MarketMan to update each product price. Runs are deduplicated per email, leave no partial state if approval is rejected (the run halts before any write), and re-running is safe because each price update targets a specific product code.
Prerequisites
- A MarketMan connection added under Connections with access to your vendor catalog. See the MarketMan connector overview.
- Your MarketMan vendor GUID (the connector uses a default if one is configured, otherwise supply it per call).
- A consistent join key between the supplier CSV and your MarketMan products, for example a vendor product code or SKU column. Without a reliable key the mapping step cannot be trusted.
- At least one user, role, or team to act as the price-change approver, plus the
/approvalsinbox enabled for them. - The supplier mail (or a forwarding rule) able to send to a custom address. Mailhook does not poll a mailbox, so you control where the mail is sent.
Step 1: Receive the price-list email with a Mailhook trigger
Add a Trigger node and set Trigger Type to Mailhook. Set an Address prefix such as prices, click Generate email address, and copy the generated address (it looks like prices-1a2b3c4d5e6f7g8h@mailhook.spojit.com). Give that address to the supplier, or add a forwarding rule in your accounts inbox so price-list emails are auto-forwarded to it. To keep stray mail out, set the optional From allowlist to the supplier's sending domain and a Subject regex such as price|catalog. The trigger output is available as {{ input }} and includes from, subject, text, replyTo, and an attachments array of references. For the difference between this and a polled mailbox, see when to use a mailhook.
Step 2: Fetch the CSV with an Attachment node
Add an Attachment node (the designer only allows this node when the trigger is a Mailhook). Set Mode to Single so you get the first matching file as an object, set the Content type filter to text/csv, and set the Filename pattern to a glob like *.csv so a logo or signature image is never picked up. Turn on Fail if no attachment matches so a price email with no spreadsheet stops the run cleanly instead of continuing with empty data. The node output is { filename, contentType, size, content }, where content is the base64 file you will hand to the csv parser. Keep in mind the default limits of 10 MB per attachment and 25 MB per run.
Step 3: Parse the rows with the csv connector
Add a Connector node, choose the csv connector in Direct mode, and pick the parse tool. Map its input to the attachment bytes using {{ attachment.content }} (decode the base64 first with the encoding connector's base64-decode tool if your supplier sends raw bytes rather than text). The parse tool returns an array of row objects keyed by the CSV header. If the supplier ships extra columns you do not need, follow with the csv pick tool to keep only the columns that matter, for example the product code, description, pack size, and new unit price. A quick info call is handy to confirm the row count before you spend AI credits on mapping.
Step 4: Map rows to MarketMan products with Agent mode
First read your live catalog so the agent has something to match against: add a Connector node with the MarketMan connector in Direct mode and call vendor-get-products (optionally pass vendorGuid) to retrieve the vendor's current product list. Then add a second MarketMan Connector node and switch it to Agent mode. In the prompt, give the agent both the parsed supplier rows and the MarketMan product list and ask it to match each supplier line to a MarketMan product by code, flagging any row it cannot confidently match. Use a Response Schema to force clean, structured output so the rest of the workflow stays deterministic, for example:
{
"type": "object",
"properties": {
"updates": {
"type": "array",
"items": {
"type": "object",
"properties": {
"productCode": { "type": "string" },
"productName": { "type": "string" },
"currentPrice": { "type": "number" },
"newPrice": { "type": "number" },
"matched": { "type": "boolean" }
},
"required": ["productCode", "newPrice", "matched"]
}
},
"unmatched": { "type": "array", "items": { "type": "string" } }
},
"required": ["updates", "unmatched"]
}
Agent mode is the right choice here because matching loose supplier descriptions to your catalog needs judgment. For the trade-offs, see choosing between Agent and Direct mode.
Step 5: Pause for human approval
Add a Human node so no price is written until a person signs off. Set a clear Label like Approve supplier price changes and a Message that summarises the change set, pulling values from the previous step, for example Supplier {{ input.from }} sent {{ mapping.updates }} price updates; {{ mapping.unmatched }} rows could not be matched. Add an Approval slot with the user, role, or team that owns purchasing, set a Timeout (minutes) if you want stale requests to lapse, and optionally turn on Email approvers. The approver acts in the /approvals inbox. If they approve, the run continues with output { approved: true, outcome: "APPROVED" }; if they reject or it times out, the run halts before any MarketMan write, so nothing changes. See using Human approval nodes.
Step 6: Update MarketMan pricing after approval
Add a Loop node set to ForEach over {{ mapping.updates }} so you process each approved row. Inside the loop, add a Condition node that only continues when {{ item.matched }} is true, then add a MarketMan Connector node in Direct mode calling vendor-set-product. Pass the vendor product object in productData with the matched productCode and the new price, for example:
{
"vendorGuid": "{{ vendorGuid }}",
"productData": {
"ProductCode": "{{ item.productCode }}",
"Price": {{ item.newPrice }}
}
}
The vendor-set-product tool creates or updates a vendor product, so it is the right call for applying the revised price. If you also want a confirmation trail, add a Send Email node after the loop that replies to {{ input.replyTo }} with a summary of what was applied and which rows were skipped.
Step 7: Handle unmatched rows
Rows that the agent could not confidently match should never be guessed. After the loop, add a Condition node that checks whether {{ mapping.unmatched }} is non-empty, and on the true branch add a Send Email node to your purchasing team listing the unmatched supplier lines so someone can map them by hand in MarketMan. This keeps the automated path clean while making sure no price change is silently dropped.
Tips
- Run
vendor-get-productsfresh on every execution rather than caching it, so the Agent step always matches against the current catalog and never reuses a stale price. - Use the csv
infotool early to assert a sensible row count, and add a Condition to skip the AI step entirely if the file is empty, saving credits. - Keep the Agent prompt tight: name the join column explicitly (for example "match on the SKU column") so the agent does not invent fuzzy matches.
- Put the price summary directly in the Human node Message so approvers can decide without opening attachments.
Common Pitfalls
- No Mailhook trigger. The Attachment node will refuse to save unless the trigger is a Mailhook. Add and configure the trigger first.
- Picking the wrong file. Without a
text/csvcontent-type filter and a*.csvfilename pattern, the Attachment node can grab an email signature image. Always constrain both. - Inconsistent join keys. If the supplier renames or reorders columns, the mapping degrades. Lean on the Response Schema
matchedflag and the unmatched list rather than trusting every row. - Rejection has no fallback branch. A rejected or timed-out Human node halts the run; downstream "on reject do X" branching is not supported. Send any heads-up email before the approval, or run a separate notification workflow.
- Attachment size limits. Large multi-tab exports can exceed the 10 MB per-attachment or 25 MB per-run limits. Ask suppliers for a plain CSV, not a workbook.
Testing
Before turning this on for a real supplier, send a test email with a small CSV of two or three known products to your mailhook address. Open the run in execution history and confirm the Attachment node returned the file, the csv parse output has the expected rows, and the Agent step produced a clean updates array with correct matched flags. Let the Human node fire and approve it yourself from /approvals, then verify in MarketMan that only those test products changed price. Once the small run is correct end to end, point the live supplier address at the mailhook.