Reads each customer's purchases and feedback, lets AI decide who earns a coupon, and sends a tailored email from Gmail.
Uses
Claude
Gmail
Google Sheets
Personalised marketing emails from customer data
sheetUrl to blank.Set sheetUrl to blank.sheetTabName to blank.Set sheetTabName to blank.businessName to blank.Set businessName to blank.senderName to blank.Set senderName to blank.emailTone to warm and friendly.Set emailTone to warm and friendly.emailGoal to bring them back for their next order.Set emailGoal to bring them back for their next order.couponCode to blank.Set couponCode to blank.couponDetails to blank.Set couponDetails to blank.couponPolicy to Offer the coupon when the feedback is negative or mixed, or when a customer who has ordered more than once has not ordered for over 60 days. Do not offer it to a happy customer who ordered recently - they will come back anyway..Set couponPolicy to Offer the coupon when the feedback is negative or mixed, or when a customer who has ordered more than once has not ordered for over 60 days. Do not offer it to a happy customer who ordered recently - they will come back anyway..onlySegment to blank.Set onlySegment to blank.optOutLine to blank.Set optOutLine to blank.sentLogTabName to Emails sent.Set sentLogTabName to Emails sent.maxEmailsPerRun to 1.Set maxEmailsPerRun to 1.sentLogIsNew to true.Set sentLogIsNew to true.createLogTabErrorIf a step below fails, save the error as createLogTabErrorTry
sentLogTabName in sheetUrl.Create the tab sentLogTabName in sheetUrl.On error
sentLogIsNew to false.Change sentLogIsNew to false.sentLogIsNew is yesIf sentLogIsNew is yesYes
sentLogTabName in sheetUrl.Add a row to sentLogTabName in sheetUrl.No
No steps
alreadyEmailed to an empty list.Set alreadyEmailed to an empty list.sentLogKeys to an empty list.Set sentLogKeys to an empty list.sentLogReadErrorIf a step below fails, save the error as sentLogReadErrorTry
sentLogTabName in sheetUrl. Save it as sentLog.Read column 'A' of sentLogTabName in sheetUrl. Save it as sentLog.sentLogKeys to sentLog.values.Change sentLogKeys to sentLog.values.On error
loggedKey in sentLogKeys:For each loggedKey in sentLogKeys:loggedKeyErrorIf a step below fails, save the error as loggedKeyErrorTry
loggedKey in lowercase to the end of alreadyEmailed and save it as alreadyEmailed.Add loggedKey in lowercase to the end of alreadyEmailed and save it as alreadyEmailed.On error
sentThisRun to 0.Set sentThisRun to 0.couponsSent to 0.Set couponsSent to 0.alreadyEmailedRows to 0.Set alreadyEmailedRows to 0.outsideSegmentRows to 0.Set outsideSegmentRows to 0.skippedRows to 0.Set skippedRows to 0.sendFailures to 0.Set sendFailures to 0.onlySegment lowercase and save it as segmentFilter.Make onlySegment lowercase and save it as segmentFilter.sheetUrl. Save it as customerSheet.Read the rows of sheetUrl. Save it as customerSheet.customerSheet.count is 0If customerSheet.count is 0Yes
No
No steps
row in customerSheet.rows:For each row in customerSheet.rows:sentThisRun is greater than or equal to maxEmailsPerRunIf sentThisRun is greater than or equal to maxEmailsPerRunYes
No
No steps
rowLabel to row row.row_number as text.Set rowLabel to row row.row_number as text.customerName to blank.Set customerName to blank.customerEmail to blank.Set customerEmail to blank.product to blank.Set product to blank.rowIsReadable to true.Set rowIsReadable to true.rowErrorIf a step below fails, save the error as rowErrorTry
row.Name and save it as customerName.Trim row.Name and save it as customerName.row.Email and save it as customerEmail.Trim row.Email and save it as customerEmail.row.Product and save it as product.Trim row.Product and save it as product.On error
rowIsReadable to false.Change rowIsReadable to false.rowIsReadable is noIf rowIsReadable is noYes
rowLabel: the sheet needs Name, Email and Product columns, spelled exactly like that, and one of them is missing.Show in the output: Skipped rowLabel: the sheet needs Name, Email and Product columns, spelled exactly like that, and one of them is missing.skippedRows.Add 1 to skippedRows.No
No steps
orderTotal to blank.Set orderTotal to blank.orderTotalErrorIf a step below fails, save the error as orderTotalErrorTry
row["Order total"] and save it as orderTotal.Trim row["Order total"] and save it as orderTotal.On error
orderTotal to blank.Change orderTotal to blank.previousOrders to blank.Set previousOrders to blank.previousOrdersErrorIf a step below fails, save the error as previousOrdersErrorTry
row["Previous orders"] and save it as previousOrders.Trim row["Previous orders"] and save it as previousOrders.On error
previousOrders to blank.Change previousOrders to blank.lastOrderDate to blank.Set lastOrderDate to blank.lastOrderDateErrorIf a step below fails, save the error as lastOrderDateErrorTry
row["Last order date"] and save it as lastOrderDate.Trim row["Last order date"] and save it as lastOrderDate.On error
lastOrderDate to blank.Change lastOrderDate to blank.feedback to blank.Set feedback to blank.feedbackErrorIf a step below fails, save the error as feedbackErrorTry
row.Feedback and save it as feedback.Trim row.Feedback and save it as feedback.On error
feedback to blank.Change feedback to blank.segmentFilter isn't emptyIf segmentFilter isn't emptyYes
rowSegment to blank.Set rowSegment to blank.segmentIsReadable to true.Set segmentIsReadable to true.segmentErrorIf a step below fails, save the error as segmentErrorTry
row.Segment lowercase and save it as rowSegment.Make row.Segment lowercase and save it as rowSegment.On error
segmentIsReadable to false.Change segmentIsReadable to false.segmentIsReadable is noIf segmentIsReadable is noYes
rowLabel: 'onlySegment' is set but the sheet has no Segment column. Add one, or clear 'onlySegment' to email every row.Show in the output: Skipped rowLabel: 'onlySegment' is set but the sheet has no Segment column. Add one, or clear 'onlySegment' to email every row.skippedRows.Add 1 to skippedRows.No
No steps
rowSegment is not segmentFilterIf rowSegment is not segmentFilterYes
outsideSegmentRows.Add 1 to outsideSegmentRows.No
No steps
No
No steps
customerEmail is a valid email addressIf not customerEmail is a valid email addressYes
rowLabel: the Email cell is empty or does not hold an email address.Show in the output: Skipped rowLabel: the Email cell is empty or does not hold an email address.skippedRows.Add 1 to skippedRows.No
No steps
sendKey to customerEmail in lowercase | product in lowercase.Set sendKey to customerEmail in lowercase | product in lowercase.alreadyEmailed contains sendKeyIf alreadyEmailed contains sendKeyYes
alreadyEmailedRows.Add 1 to alreadyEmailedRows.No
No steps
customerNameForPrompt to customerName.Set customerNameForPrompt to customerName.customerNameForPrompt is emptyIf customerNameForPrompt is emptyYes
customerNameForPrompt to not known.Change customerNameForPrompt to not known.No
No steps
productForPrompt to product.Set productForPrompt to product.productForPrompt is emptyIf productForPrompt is emptyYes
productForPrompt to not recorded - refer to it as their recent purchase.Change productForPrompt to not recorded - refer to it as their recent purchase.No
No steps
orderTotal is emptyIf orderTotal is emptyYes
orderTotal to not recorded.Change orderTotal to not recorded.No
No steps
previousOrders is emptyIf previousOrders is emptyYes
previousOrders to not recorded.Change previousOrders to not recorded.No
No steps
lastOrderDate is emptyIf lastOrderDate is emptyYes
lastOrderDate to not recorded.Change lastOrderDate to not recorded.No
No steps
feedback to 1500 characters and save it as feedbackForPrompt.Shorten feedback to 1500 characters and save it as feedbackForPrompt.feedbackForPrompt is emptyIf feedbackForPrompt is emptyYes
feedbackForPrompt to none left.Change feedbackForPrompt to none left.No
No steps
customerContext to Business: businessNameSet customerContext to Business: businessName
Customer's name: customerNameForPrompt
What they bought: productForPrompt
Order total: orderTotal
Previous orders with us: previousOrders
Last order date: lastOrderDate
Today's date: today's date
Feedback they left: feedbackForPrompt
Tone to write in: emailTone
Sign off as: senderName.assessmentPrompt to You are helping businessName personalise a marketing email to one customer. Read their purchase details and the feedback they left, then decide two things: the overall sentiment of their feedback, and whether this email should include a coupon.Set assessmentPrompt to You are helping businessName personalise a marketing email to one customer. Read their purchase details and the feedback they left, then decide two things: the overall sentiment of their feedback, and whether this email should include a coupon.
customerContext
Coupon policy: couponPolicy
Reply with exactly three lines and nothing else:
Sentiment: positive, neutral or negative (neutral when no feedback was left or it is too little to tell)
Coupon: yes or no
Reason: one plain sentence, under 30 words, saying why this customer should or should not get the coupon.assessmentPrompt. Save it as assessmentAnswer.Ask Claude: assessmentPrompt. Save it as assessmentAnswer.
Maximum response length: 200sentiment to blank.Set sentiment to blank.couponDecision to blank.Set couponDecision to blank.couponReason to blank.Set couponReason to blank.assessmentErrorIf a step below fails, save the error as assessmentErrorTry
assessmentText to assessmentAnswer.response_text.Set assessmentText to assessmentAnswer.response_text.assessmentText lowercase and save it as sentiment.Make the first match in assessmentText lowercase and save it as sentiment.assessmentText lowercase and save it as couponDecision.Make the first match in assessmentText lowercase and save it as couponDecision.assessmentText truncated to 300 characters and save it as couponReason.Trim the first match in assessmentText truncated to 300 characters and save it as couponReason.On error
sentiment to blank.Change sentiment to blank.couponDecision to blank.Change couponDecision to blank.sentiment is not positive and sentiment is not neutral and sentiment is…If sentiment is not positive and sentiment is not neutral and sentiment is…Yes
rowLabel (customerEmail): could not tell whether the feedback is positive, neutral or negative. It will be tried again on the next run.Show in the output: Skipped rowLabel (customerEmail): could not tell whether the feedback is positive, neutral or negative. It will be tried again on the next run.skippedRows.Add 1 to skippedRows.No
No steps
couponDecision is not yes and couponDecision is not noIf couponDecision is not yes and couponDecision is not noYes
rowLabel (customerEmail): no clear yes or no came back on the coupon. It will be tried again on the next run.Show in the output: Skipped rowLabel (customerEmail): no clear yes or no came back on the coupon. It will be tried again on the next run.skippedRows.Add 1 to skippedRows.No
No steps
couponReason is emptyIf couponReason is emptyYes
couponReason to no reason given.Change couponReason to no reason given.No
No steps
subject to blank.Set subject to blank.body to blank.Set body to blank.couponFlag to no.Set couponFlag to no.couponDecision is yesIf couponDecision is yesYes
couponLine to Coupon code couponCode - couponDetails.Set couponLine to Coupon code couponCode - couponDetails.couponSubjectPrompt to Write the subject line for a short marketing email from businessName to one customer.Set couponSubjectPrompt to Write the subject line for a short marketing email from businessName to one customer.
customerContext
The email includes a coupon for their next order. Why it is being offered: couponReason
If their feedback was negative, the email apologises and makes it right; otherwise it thanks them and gives them a reason to come back. Do not put the coupon code in the subject line.
Rules: under 60 characters, no quotation marks, no emoji, no placeholders such as [Name]. Write in the tone given above. Reply with the subject line only.couponSubjectPrompt. Save it as couponSubjectAnswer.Ask Claude: couponSubjectPrompt. Save it as couponSubjectAnswer.
Maximum response length: 60couponSubjectErrorIf a step below fails, save the error as couponSubjectErrorTry
couponSubjectAnswer.response_text and save it as subject.Trim couponSubjectAnswer.response_text and save it as subject.On error
subject to blank.Change subject to blank.couponBodyPrompt to Write the body of a short marketing email (under 170 words) from businessName to one customer.Set couponBodyPrompt to Write the body of a short marketing email (under 170 words) from businessName to one customer.
customerContext
Goal of the email: emailGoal.
Why this customer is getting a coupon: couponReason
If their feedback was negative, apologise sincerely and specifically for what they described, without being defensive or making excuses, and thank them for telling you. Otherwise thank them for their custom and respond in a sentence to anything they said. Then offer them this coupon for their next order, quoting the code exactly as written: couponLine.
Rules: plain text only - no subject line, no markdown, no bullet points and no placeholders such as [Name]. Write in the tone given above. If the customer's name is not known, open with a plain greeting rather than inventing one. Use only the facts given above - do not invent details about the product, the order or the customer. Do not promise a refund, a replacement or anything beyond the coupon. Sign off as senderName. Reply with the email body only.couponBodyPrompt. Save it as couponBodyAnswer.Ask Claude: couponBodyPrompt. Save it as couponBodyAnswer.
Maximum response length: 700couponBodyErrorIf a step below fails, save the error as couponBodyErrorTry
couponBodyAnswer.response_text and save it as body.Trim couponBodyAnswer.response_text and save it as body.On error
body to blank.Change body to blank.body isn't empty and body doesn't contain couponCode (ignoring case)If body isn't empty and body doesn't contain couponCode (ignoring case)Yes
body to bodyChange body to body
couponLine.No
No steps
couponFlag to yes.Change couponFlag to yes.No
plainSubjectPrompt to Write the subject line for a short marketing email from businessName to one customer.Set plainSubjectPrompt to Write the subject line for a short marketing email from businessName to one customer.
customerContext
The email thanks them for their custom, answers anything they said in their feedback, and works towards this goal: emailGoal. No discount or coupon is offered.
Rules: under 60 characters, no quotation marks, no emoji, no placeholders such as [Name]. Write in the tone given above. Reply with the subject line only.plainSubjectPrompt. Save it as plainSubjectAnswer.Ask Claude: plainSubjectPrompt. Save it as plainSubjectAnswer.
Maximum response length: 60plainSubjectErrorIf a step below fails, save the error as plainSubjectErrorTry
plainSubjectAnswer.response_text and save it as subject.Trim plainSubjectAnswer.response_text and save it as subject.On error
subject to blank.Change subject to blank.plainBodyPrompt to Write the body of a short marketing email (under 150 words) from businessName to one customer.Set plainBodyPrompt to Write the body of a short marketing email (under 150 words) from businessName to one customer.
customerContext
Goal of the email: emailGoal.
Thank them for their purchase and respond in a sentence or two to anything they said in their feedback. If the feedback was negative, apologise for what they described and say what you will do better, without offering compensation. Then work towards the goal above.
Rules: plain text only - no subject line, no markdown, no bullet points and no placeholders such as [Name]. Write in the tone given above. If the customer's name is not known, open with a plain greeting rather than inventing one. Use only the facts given above - do not invent details about the product, the order or the customer. Do not offer a discount, a coupon or a refund. Sign off as senderName. Reply with the email body only.plainBodyPrompt. Save it as plainBodyAnswer.Ask Claude: plainBodyPrompt. Save it as plainBodyAnswer.
Maximum response length: 700plainBodyErrorIf a step below fails, save the error as plainBodyErrorTry
plainBodyAnswer.response_text and save it as body.Trim plainBodyAnswer.response_text and save it as body.On error
body to blank.Change body to blank.subject with replaced by " and save it as subject.Trim subject with replaced by " and save it as subject.body with ** replaced by and save it as body.Trim body with ** replaced by and save it as body.subject is empty or body is emptyIf subject is empty or body is emptyYes
rowLabel (customerEmail): no usable subject line or email body came back. It will be tried again on the next run.Show in the output: Skipped rowLabel (customerEmail): no usable subject line or email body came back. It will be tried again on the next run.skippedRows.Add 1 to skippedRows.No
No steps
optOutLine isn't blankIf optOutLine isn't blankYes
body to bodyChange body to body
optOutLine.No
No steps
emailSent to true.Set emailSent to true.sendErrorIf a step below fails, save the error as sendErrorTry
customerEmail.Send an email to customerEmail.
Subject: subjectbodyOn error
emailSent to false.Change emailSent to false.customerEmail (rowLabel): sendError.message.Show in the output: Could not send to customerEmail (rowLabel): sendError.message.emailSent is noIf emailSent is noYes
sendFailures.Add 1 to sendFailures.No
No steps
logWriteErrorIf a step below fails, save the error as logWriteErrorTry
sentLogTabName in sheetUrl.Add a row to sentLogTabName in sheetUrl.On error
customerEmail but could not add it to the '@sentLogTabName' tab. The run stopped here so that nobody is emailed twice on the next run - fix the spreadsheet, then run it again.Stop and report a problem: Sent the email to customerEmail but could not add it to the '@sentLogTabName' tab. The run stopped here so that nobody is emailed twice on the next run - fix the spreadsheet, then run it again.sendKey to the end of alreadyEmailed and save it as alreadyEmailed.Add sendKey to the end of alreadyEmailed and save it as alreadyEmailed.sentThisRun.Add 1 to sentThisRun.couponFlag is yesIf couponFlag is yesYes
couponsSent.Add 1 to couponsSent.No
No steps
sentThisRun as text email(s) this run, couponsSent as text with a coupon. alreadyEmailedRows as text row(s) had already been emailed, outsideSegmentRows as text row(s) were outside the chosen segment, skippedRows as text row(s) were skipped and sendFailures as text send(s) failed. Raise 'maxEmailsPerRun' to send more in one run.Show in the output: Sent sentThisRun as text email(s) this run, couponsSent as text with a coupon. alreadyEmailedRows as text row(s) had already been emailed, outsideSegmentRows as text row(s) were outside the chosen segment, skippedRows as text row(s) were skipped and sendFailures as text send(s) failed. Raise 'maxEmailsPerRun' to send more in one run.