Reads each new purchase order email, looks its items up in your product list, and logs every line with name and price.
Uses
Claude
Gmail
Google Sheets
Extract purchase orders from Gmail
When a new email arrives in your Gmail inbox, the AI checks whether it is a purchase order. If it is, it copies the PO number, customer, dates and each line's SKU, description and quantity exactly as written. Each SKU is looked up in the Products tab of your spreadsheet, and one row per line is added to your Purchase orders tab with the product name, unit price and a line total. Emails that are not orders - replies chasing an earlier order, quotes, invoices, newsletters - are skipped, and an email already in the log is not logged again.
The workflow refuses to guess. A SKU missing from your product list, a missing quantity or PO number, a price that could be read two ways, or an email the AI was not sure about goes in the line's Needs attention column rather than being filled in. The AI reads the email's text, not its attachments, so an order sent as an attached PDF still gets a row saying its lines need adding by hand.
You need a spreadsheet with a Products tab headed SKU, Product name and Unit price, and a Purchase orders tab for the log. Each new email starts its own run; if your orders all come from one address or domain, put it in the trigger's sender filter to keep the rest of your inbox away from the AI.
email.When an email arrives. Save it as email.spreadsheetUrl to blank.Set spreadsheetUrl to blank.productsTab to Products.Set productsTab to Products.ordersTab to Purchase orders.Set ordersTab to Purchase orders.email.body to 40000 characters and save it as emailText.Shorten email.body to 40000 characters and save it as emailText.cutNote to blank.Set cutNote to blank.email.body is greater than 40000If the length of email.body is greater than 40000cutNote to The email was too long to read in full, so only its first 40,000 characters were read: check no lines are missing..Change cutNote to The email was too long to read in full, so only its first 40,000 characters were read: check no lines are missing..aiReply to blank.Set aiReply to blank.aiErrorIf a step below fails, save the error as aiErroremail.from
SUBJECT
email.subject
RECEIVED
email.date
EMAIL
emailText
Reply with one JSON object and nothing else - no code fence, no commentary before or after it. Use exactly these keys:
{is_purchase_order: true or false, certain: true or false, reason: ..., po_number: ..., customer: ..., order_date: ..., delivery_date: ..., note: ..., lines: [{sku: ..., description: ..., quantity: a number or null}]}
Rules:
- is_purchase_order: true only for a purchase order as described above. When it is false, put one short sentence in reason saying what the email is instead, and leave lines empty.
- certain: false when you are not sure the email is a purchase order. When in doubt, count it as one and set certain to false: a person can delete an extra row, but a missed order is lost. You cannot see attachments, so an email that says its order is attached - 'please find our PO attached', or only 'see attached' - counts as a purchase order with certain false and no lines.
- Copy every value exactly as the email writes it. Never guess, convert, total, or fill in a value that merely sounds right. A value the email does not give is an empty string.
- po_number: the buyer's purchase order number or order reference.
- customer: the company placing the order, or the person when no company is named.
- order_date and delivery_date: the date of the order and the delivery date the buyer asks for, as the email writes them. Leave order_date empty when the email itself gives no order date: the RECEIVED date above is when it arrived, not an order date.
- note: an empty string, unless the email says this order replaces, changes or repeats an earlier one - then say which in a few words, such as 'Replaces PO 4512'.
- lines: one entry per item ordered, in the email's order. sku is the product code, item number or part number as written, or an empty string when the line has none. description is the item's name or description as written. quantity is how many were ordered, as a JSON number with no units, no thousands separators and a dot for any decimals (1500, not 1,500; 2.5, not 2,5), reading the email's numbers the way the email itself writes them; null when the email does not say or you cannot tell.'. Save it as extraction.
Maximum response length: 8000aiReply to extraction.response_text.Change aiReply to extraction.response_text.email.from, so nothing was logged: aiError.message.Stop and report a problem: The AI could not read the email '@email.subject' from email.from, so nothing was logged: aiError.message.reply, standing_note, and received and save it as order.Run Python code on reply, standing_note, and received and save it as order.order.problem isn't emptyIf order.problem isn't emptyemail.from: order.problem.Stop and report a problem: Could not log the email '@email.subject' from email.from: order.problem.order.is_purchase_order is noIf order.is_purchase_order is noorder.reason.Finish the run: Not a purchase order, so nothing was logged. order.reason.productRows to an empty list.Set productRows to an empty list.productColumns to an empty list.Set productColumns to an empty list.productListErrorIf a step below fails, save the error as productListErrorspreadsheetUrl. Save it as productList.Read the rows of spreadsheetUrl. Save it as productList.productRows to productList.rows.Change productRows to productList.rows.productColumns to productList.headers.Change productColumns to productList.headers.email.from was not logged: productListError.message.Stop and report a problem: Could not read the '@productsTab' tab, so the purchase order in the email '@email.subject' from email.from was not logged: productListError.message.productColumns doesn't contain SKUIf productColumns doesn't contain SKUemail.from was not logged. Give the tab the headers SKU, Product name and Unit price in its first row.Stop and report a problem: The '@productsTab' tab needs a column headed exactly 'SKU', so the purchase order in the email '@email.subject' from email.from was not logged. Give the tab the headers SKU, Product name and Unit price in its first row.productColumns doesn't contain Product nameIf productColumns doesn't contain Product nameemail.from was not logged. Give the tab the headers SKU, Product name and Unit price in its first row.Stop and report a problem: The '@productsTab' tab needs a column headed exactly 'Product name', so the purchase order in the email '@email.subject' from email.from was not logged. Give the tab the headers SKU, Product name and Unit price in its first row.productColumns doesn't contain Unit priceIf productColumns doesn't contain Unit priceemail.from was not logged. Give the tab the headers SKU, Product name and Unit price in its first row.Stop and report a problem: The '@productsTab' tab needs a column headed exactly 'Unit price', so the purchase order in the email '@email.subject' from email.from was not logged. Give the tab the headers SKU, Product name and Unit price in its first row.loggedIds to an empty list.Set loggedIds to an empty list.loggedIdsErrorIf a step below fails, save the error as loggedIdsErrorordersTab in spreadsheetUrl. Save it as loggedColumn.Read column 'M' of ordersTab in spreadsheetUrl. Save it as loggedColumn.loggedIds to loggedColumn.values.Change loggedIds to loggedColumn.values.email.from is already logged, so its purchase order was not logged: loggedIdsError.message.Stop and report a problem: Could not read the '@ordersTab' tab to check whether the email '@email.subject' from email.from is already logged, so its purchase order was not logged: loggedIdsError.message.loggedIds contains email.email_idIf loggedIds contains email.email_idemail.from is in the '@ordersTab' tab under its Gmail message ID, so it was not logged again.Finish the run: Already logged: the email '@email.subject' from email.from is in the '@ordersTab' tab under its Gmail message ID, so it was not logged again.received to email.date.Set received to email.date.order.received_iso isn't blankIf order.received_iso isn't blankT with in order.received_iso formatted as an ISO 8601 timestamp truncated to 16 characters using as the ellipsis and save it as received.Replace T with in order.received_iso formatted as an ISO 8601 timestamp truncated to 16 characters using as the ellipsis and save it as received.lines, skus, names, prices, order, received, and email_id and save it as priced.Run Python code on lines, skus, names, prices, order, received, and email_id and save it as priced.rowsToLog to priced.rows.Set rowsToLog to priced.rows.rowsToLog and save it as lineCount.Count the items in rowsToLog and save it as lineCount.linesWritten to 0.Set linesWritten to 0.logErrorIf a step below fails, save the error as logErrorrowToAdd in rowsToLog:For each rowToAdd in rowsToLog:ordersTab in spreadsheetUrl. Save it as logged. Add a row to ordersTab in spreadsheetUrl. Save it as logged.linesWritten. Add 1 to linesWritten.linesWritten of the lineCount lines of the purchase order in the email '@email.subject' from email.from were added to the '@ordersTab' tab: logError.message. Add the missing lines by hand: once any line of an email is in the tab, the workflow treats that email as logged and does not add the rest.Stop and report a problem: Google Sheets stopped after linesWritten of the lineCount lines of the purchase order in the email '@email.subject' from email.from were added to the '@ordersTab' tab: logError.message. Add the missing lines by hand: once any line of an email is in the tab, the workflow treats that email as logged and does not add the rest.lineCount line(s) from the email '@email.subject' to the '@ordersTab' tab.Finish the run: Logged lineCount line(s) from the email '@email.subject' to the '@ordersTab' tab.