Writes a different first-contact email for each row of your sheet from that row's own facts, records it, then sends it.

UsesClaudeGmailGoogle Sheets

Write a tailored first email for every row of your list

ValuesSet sheetUrl to blank.Set sheetUrl to blank.
ValuesSet prospectTabName to blank.Set prospectTabName to blank.
ValuesSet sentLogTabName to blank.Set sentLogTabName to blank.
ValuesSet whatYouOffer to blank.Set whatYouOffer to blank.
ValuesSet senderSignOff to blank.Set senderSignOff to blank.
ValuesSet optOutLine to blank.Set optOutLine to blank.
ValuesSet maxEmailsThisRun to 1.Set maxEmailsThisRun to 1.
MathSet sendLimit to maxEmailsThisRun multiplied by 1.Set sendLimit to maxEmailsThisRun multiplied by 1.
Decisions & LoopsIf sendLimit is less than 1 or sendLimit is greater than 200If sendLimit is less than 1 or sendLimit is greater than 200
Yes
Decisions & LoopsStop and report a problem: 'maxEmailsThisRun' must be a whole number between 1 and 200. Set it in the 'maxEmailsThisRun' step and run it again.Stop and report a problem: 'maxEmailsThisRun' must be a whole number between 1 and 200. Set it in the 'maxEmailsThisRun' step and run it again.
No
No steps
ValuesSet sentLogIsNew to true.Set sentLogIsNew to true.
Decisions & LoopsIf a step below fails, save the error as createLogTabErrorIf a step below fails, save the error as createLogTabError
Try
Google SheetsGoogle SheetsCreate the tab sentLogTabName in sheetUrl.Create the tab sentLogTabName in sheetUrl.
On error
ValuesChange sentLogIsNew to false.Change sentLogIsNew to false.
Decisions & LoopsIf sentLogIsNew is yesIf sentLogIsNew is yes
Yes
Google SheetsGoogle SheetsAdd a row to sentLogTabName in sheetUrl.Add a row to sentLogTabName in sheetUrl.
No
No steps
Google SheetsGoogle SheetsRead column 'A' of sentLogTabName in sheetUrl. Save it as sentLog.Read column 'A' of sentLogTabName in sheetUrl. Save it as sentLog.
ValuesSet alreadySent to an empty list.Set alreadySent to an empty list.
Decisions & LoopsFor each loggedAddress in sentLog.values:For each loggedAddress in sentLog.values:
Decisions & LoopsIf a step below fails, save the error as loggedAddressErrorIf a step below fails, save the error as loggedAddressError
Try
ListsAdd loggedAddress in lowercase to the end of alreadySent and save it as alreadySent.Add loggedAddress in lowercase to the end of alreadySent and save it as alreadySent.
On error
ValuesShow in the output: Ignored a cell in the '@sentLogTabName' tab that is not text.Show in the output: Ignored a cell in the '@sentLogTabName' tab that is not text.
Google SheetsGoogle SheetsRead the rows of sheetUrl. Save it as prospectSheet.Read the rows of sheetUrl. Save it as prospectSheet.
ValuesSet sentCount to 0.Set sentCount to 0.
ValuesSet skippedRows to 0.Set skippedRows to 0.
ValuesSet sendFailures to 0.Set sendFailures to 0.
Decisions & LoopsFor each row in prospectSheet.rows:For each row in prospectSheet.rows:
Decisions & LoopsIf sentCount is greater than or equal to sendLimitIf sentCount is greater than or equal to sendLimit
Yes
Decisions & LoopsStop repeating.Stop repeating.
No
No steps
ValuesSet rowLabel to row row.row_number as text.Set rowLabel to row row.row_number as text.
ValuesSet recipient to blank.Set recipient to blank.
Decisions & LoopsIf a step below fails, save the error as recipientReadErrorIf a step below fails, save the error as recipientReadError
Try
TextTrim row.Email and save it as recipient.Trim row.Email and save it as recipient.
On error
ValuesChange recipient to blank.Change recipient to blank.
Decisions & LoopsIf recipient is emptyIf recipient is empty
Yes
ValuesShow in the output: Skipped rowLabel: the Email column is empty.Show in the output: Skipped rowLabel: the Email column is empty.
ValuesAdd 1 to skippedRows.Add 1 to skippedRows.
Decisions & LoopsSkip to the next item.Skip to the next item.
No
No steps
Decisions & LoopsIf not recipient is a valid email addressIf not recipient is a valid email address
Yes
ValuesShow in the output: Skipped rowLabel: '@recipient' is not a valid email address, so nothing was sent to it.Show in the output: Skipped rowLabel: '@recipient' is not a valid email address, so nothing was sent to it.
ValuesAdd 1 to skippedRows.Add 1 to skippedRows.
Decisions & LoopsSkip to the next item.Skip to the next item.
No
No steps
TextMake recipient lowercase and save it as recipientKey.Make recipient lowercase and save it as recipientKey.
Decisions & LoopsIf alreadySent contains recipientKeyIf alreadySent contains recipientKey
Yes
ValuesShow in the output: Skipped recipient: already on the sent log, so they are not emailed twice.Show in the output: Skipped recipient: already on the sent log, so they are not emailed twice.
ValuesAdd 1 to skippedRows.Add 1 to skippedRows.
Decisions & LoopsSkip to the next item.Skip to the next item.
No
No steps
ValuesSet contactName to blank.Set contactName to blank.
Decisions & LoopsIf a step below fails, save the error as contactNameReadErrorIf a step below fails, save the error as contactNameReadError
Try
TextTrim row.Name and save it as contactName.Trim row.Name and save it as contactName.
On error
ValuesChange contactName to blank.Change contactName to blank.
ValuesSet companyName to blank.Set companyName to blank.
Decisions & LoopsIf a step below fails, save the error as companyNameReadErrorIf a step below fails, save the error as companyNameReadError
Try
TextTrim row.Company and save it as companyName.Trim row.Company and save it as companyName.
On error
ValuesChange companyName to blank.Change companyName to blank.
ValuesSet contactRole to blank.Set contactRole to blank.
Decisions & LoopsIf a step below fails, save the error as contactRoleReadErrorIf a step below fails, save the error as contactRoleReadError
Try
TextTrim row.Role and save it as contactRole.Trim row.Role and save it as contactRole.
On error
ValuesChange contactRole to blank.Change contactRole to blank.
ValuesSet contactNotes to blank.Set contactNotes to blank.
Decisions & LoopsIf a step below fails, save the error as contactNotesReadErrorIf a step below fails, save the error as contactNotesReadError
Try
TextTrim row.Notes and save it as contactNotes.Trim row.Notes and save it as contactNotes.
On error
ValuesChange contactNotes to blank.Change contactNotes to blank.
Decisions & LoopsIf contactName is empty or companyName is emptyIf contactName is empty or companyName is empty
Yes
ValuesShow in the output: Skipped recipient: a tailored email needs at least a Name and a Company on the row.Show in the output: Skipped recipient: a tailored email needs at least a Name and a Company on the row.
ValuesAdd 1 to skippedRows.Add 1 to skippedRows.
Decisions & LoopsSkip to the next item.Skip to the next item.
No
No steps
ValuesSet promptFacts to THIS PERSONSet promptFacts to THIS PERSON Name: contactName Company: companyName Role: contactRole What my own notes say about them: contactNotes.
ValuesSet promptOffer to WHY I AM WRITING TO THEM, in my own words:Set promptOffer to WHY I AM WRITING TO THEM, in my own words: whatYouOffer.
ValuesSet prompt to You are writing one short first-contact email to a person who has never heard from me before.Set prompt to You are writing one short first-contact email to a person who has never heard from me before. Rules you must follow exactly: - Use only the facts listed under THIS PERSON below. Do not add anything else about them, their company, their funding, their headcount, their recent news or their background. If a detail is not listed below, it does not go in the email. - Never suggest we have met, spoken, emailed, been introduced, or that they asked to hear from me. There has been no contact of any kind. - Never mention or imply anyone's age, gender, race, nationality, religion, health, disability, politics or family. - Write the way one person writes to another: 120 words at most, plain sentences, one clear ask, no bullet points, no marketing language, no flattery you cannot support. - Do not write a sign-off, a signature or an opt-out line. Those are added afterwards. Reply in exactly this shape and nothing else: Subject: <a specific subject line, under 70 characters> <blank line> <the body of the email> promptFacts promptOffer.
ClaudeClaudeAsk Claude: prompt. Save it as drafted.Ask Claude: prompt. Save it as drafted. Maximum response length: 700
ValuesSet draftText to blank.Set draftText to blank.
Decisions & LoopsIf a step below fails, save the error as draftReadErrorIf a step below fails, save the error as draftReadError
Try
TextTrim drafted.response_text and save it as draftText.Trim drafted.response_text and save it as draftText.
On error
ValuesChange draftText to blank.Change draftText to blank.
Decisions & LoopsIf draftText is empty or draftText is nullIf draftText is empty or draftText is null
Yes
ValuesShow in the output: Skipped recipient: the model returned no draft.Show in the output: Skipped recipient: the model returned no draft.
ValuesAdd 1 to skippedRows.Add 1 to skippedRows.
Decisions & LoopsSkip to the next item.Skip to the next item.
No
No steps
TextSplit draftText by a new line and save it as draftLines.Split draftText by a new line and save it as draftLines.
TextTrim the first item of draftLines with Subject: replaced by (ignoring case) and save it as emailSubject.Trim the first item of draftLines with Subject: replaced by (ignoring case) and save it as emailSubject.
TextTrim the items from index 1 onward of draftLines joined by a new line and save it as emailBodyText.Trim the items from index 1 onward of draftLines joined by a new line and save it as emailBodyText.
Decisions & LoopsIf emailSubject is empty or the length of emailSubject is greater than 120 or the…If emailSubject is empty or the length of emailSubject is greater than 120 or the…
Yes
ValuesShow in the output: Skipped recipient: the draft did not come back as a subject line and a body.Show in the output: Skipped recipient: the draft did not come back as a subject line and a body.
ValuesAdd 1 to skippedRows.Add 1 to skippedRows.
Decisions & LoopsSkip to the next item.Skip to the next item.
No
No steps
ValuesSet wholeDraft to emailSubjectSet wholeDraft to emailSubject emailBodyText.
ValuesSet claimsGroup1 to wholeDraft contains as we discussed (ignoring case) or wholeDraft contains as discussed (ignoring case) or wholeDraft contains following up on our call (ignoring case) or wholeDraft contains as promised (ignoring case) or wholeDraft contains per our conversation (ignoring case).Set claimsGroup1 to wholeDraft contains as we discussed (ignoring case) or wholeDraft contains as discussed (ignoring case) or wholeDraft contains following up on our call (ignoring case) or wholeDraft contains as promised (ignoring case) or wholeDraft contains per our conversation (ignoring case).
ValuesSet claimsGroup2 to wholeDraft contains great speaking (ignoring case) or wholeDraft contains we spoke (ignoring case) or wholeDraft contains our meeting (ignoring case) or wholeDraft contains thanks for your time (ignoring case) or wholeDraft contains since we last (ignoring case).Set claimsGroup2 to wholeDraft contains great speaking (ignoring case) or wholeDraft contains we spoke (ignoring case) or wholeDraft contains our meeting (ignoring case) or wholeDraft contains thanks for your time (ignoring case) or wholeDraft contains since we last (ignoring case).
ValuesSet claimsGroup3 to wholeDraft contains you asked me to (ignoring case) or wholeDraft contains you requested (ignoring case) or wholeDraft contains circling back (ignoring case) or wholeDraft contains as agreed (ignoring case).Set claimsGroup3 to wholeDraft contains you asked me to (ignoring case) or wholeDraft contains you requested (ignoring case) or wholeDraft contains circling back (ignoring case) or wholeDraft contains as agreed (ignoring case).
Decisions & LoopsIf claimsGroup1 is yes or claimsGroup2 is yes or claimsGroup3 is yesIf claimsGroup1 is yes or claimsGroup2 is yes or claimsGroup3 is yes
Yes
ValuesShow in the output: Skipped recipient: the draft implied you had already been in touch, which is not true of a first email, so nothing was sent.Show in the output: Skipped recipient: the draft implied you had already been in touch, which is not true of a first email, so nothing was sent.
ValuesAdd 1 to skippedRows.Add 1 to skippedRows.
Decisions & LoopsSkip to the next item.Skip to the next item.
No
No steps
ValuesSet emailBody to emailBodyTextSet emailBody to emailBodyText senderSignOff optOutLine.
Google SheetsGoogle SheetsAdd a row to sentLogTabName in sheetUrl.Add a row to sentLogTabName in sheetUrl.
ListsAdd recipientKey to the end of alreadySent and save it as alreadySent.Add recipientKey to the end of alreadySent and save it as alreadySent.
Decisions & LoopsIf a step below fails, save the error as sendErrorIf a step below fails, save the error as sendError
Try
GmailGmailSend an email to recipient.Send an email to recipient. Subject: emailSubjectemailBody
On error
ValuesAdd 1 to sendFailures.Add 1 to sendFailures.
ValuesShow in the output: Could not send to recipient. They are on the sent log already, so this run will not try them again: sendError as text.Show in the output: Could not send to recipient. They are on the sent log already, so this run will not try them again: sendError as text.
ValuesAdd 1 to sentCount.Add 1 to sentCount.
ValuesShow in the output: Wrote and sent sentCount as text tailored emails. Skipped skippedRows as text rows.Show in the output: Wrote and sent sentCount as text tailored emails. Skipped skippedRows as text rows.
Decisions & LoopsIf sendFailures is greater than 0If sendFailures is greater than 0
Yes
Decisions & LoopsStop and report a problem: sendFailures as text email(s) could not be sent. Every one of them is already on your sent log, so they will not be retried - check your Gmail connection and add them back by hand if you still want to reach them.Stop and report a problem: sendFailures as text email(s) could not be sent. Every one of them is already on your sent log, so they will not be retried - check your Gmail connection and add them back by hand if you still want to reach them.
No
No steps