Writes a personalised welcome email from each new client intake row, asks you to approve it, then sends it or drafts it.
Uses
Claude
Gmail
Google Sheets
Welcome new clients from their intake form
Every hour, your client intake sheet is read and a personalised welcome email is written for each new client from what they told you on the form. Each email comes to you as an approval request with the address it would go to: approve it and it goes out under your name; reject it, or leave it, and it is saved as a Gmail draft for you to edit.
Nothing reaches a client without a person approving it, and nobody is welcomed twice, even when a new run starts while another is still waiting for your answer. A row missing a name, email address or notes is skipped rather than guessed at, and the intake sheet itself is only ever read. If an approved email cannot be sent, it is saved as a draft where possible and the run is marked as failed with the client's name and the reason; if the intake tab's columns are not named Name, Email and Notes, the run stops and tells you which columns it found.
You need Google Sheets, Gmail and Anthropic connected, an intake tab with 'Name', 'Email' and 'Notes' columns, a second tab headed 'Email', 'Name', 'Outcome' and 'Welcomed at', and a short description of your firm, what it does and how you like to sound.
intakeSheetUrl to blank.Set intakeSheetUrl to blank.intakeTabName to blank.Set intakeTabName to blank.welcomedTabName to blank.Set welcomedTabName to blank.firmName to blank.Set firmName to blank.firmContext to blank.Set firmContext to blank.maxClientsPerRun to 10.Set maxClientsPerRun to 10.welcomedTabName in intakeSheetUrl. Save it as welcomedRead.Read column 'A' of welcomedTabName in intakeSheetUrl. Save it as welcomedRead.welcomedKeys to an empty list.Set welcomedKeys to an empty list.loggedAddress in welcomedRead.values:For each loggedAddress in welcomedRead.values:loggedErrorIf a step below fails, save the error as loggedErrorloggedAddress in lowercase to the end of welcomedKeys and save it as welcomedKeys.Add loggedAddress in lowercase to the end of welcomedKeys and save it as welcomedKeys.intakeSheetUrl. Save it as intake.Read the rows of intakeSheetUrl. Save it as intake.intake.headers is emptyIf intake.headers is emptyintake.headers with , and save it as foundHeaders.Join intake.headers with , and save it as foundHeaders.foundHeaders. Rename those header cells (for a Google Form, rename the questions) to Name, Email and Notes, then run it again.Stop and report a problem: The '@intakeTabName' tab needs columns named exactly Name, Email and Notes in its first row, but it has: foundHeaders. Rename those header cells (for a Google Form, rename the questions) to Name, Email and Notes, then run it again.welcomedThisRun to 0.Set welcomedThisRun to 0.stoppedAtLimit to false.Set stoppedAtLimit to false.failures to an empty list.Set failures to an empty list.row in intake.rows:For each row in intake.rows:welcomedThisRun is greater than or equal to maxClientsPerRunIf welcomedThisRun is greater than or equal to maxClientsPerRunstoppedAtLimit to true.Change stoppedAtLimit to true.rowLabel to row row.row_number as text.Set rowLabel to row row.row_number as text.clientName to blank.Set clientName to blank.clientEmail to blank.Set clientEmail to blank.clientNotes to blank.Set clientNotes to blank.rowIsComplete to false.Set rowIsComplete to false.missingColumnIf a step below fails, save the error as missingColumnrow.Name and save it as clientName.Trim row.Name and save it as clientName.row.Email and save it as clientEmail.Trim row.Email and save it as clientEmail.row.Notes and save it as clientNotes.Trim row.Notes and save it as clientNotes.rowIsComplete to the length of clientName is greater than 0 and the length of clientEmail is greater than 0 and the length of clientNotes is greater than 0.Change rowIsComplete to the length of clientName is greater than 0 and the length of clientEmail is greater than 0 and the length of clientNotes is greater than 0.rowLabel - the intake tab needs a Name, an Email and a Notes column.Show in the output: Skipping rowLabel - the intake tab needs a Name, an Email and a Notes column.rowIsComplete is noIf rowIsComplete is norowLabel - Name, Email and Notes must all be filled in before a welcome email can be written.Show in the output: Skipping rowLabel - Name, Email and Notes must all be filled in before a welcome email can be written.clientEmail is a valid email addressIf not clientEmail is a valid email addressrowLabel - '@clientEmail' is not a valid email address.Show in the output: Skipping rowLabel - '@clientEmail' is not a valid email address.clientEmail lowercase and save it as emailKey.Make clientEmail lowercase and save it as emailKey.welcomedKeys contains emailKeyIf welcomedKeys contains emailKeyclientEmail has already been welcomed - leaving rowLabel alone.Show in the output: clientEmail has already been welcomed - leaving rowLabel alone.welcomeErrorIf a step below fails, save the error as welcomeErrorprompt to You are writing the body of a welcome email to a brand new client of a professional services firm. Write only the body of the email: no subject line, no placeholder brackets such as [Name], and nothing you cannot support from the details below. Greet the client by name, refer to what they asked for in their own terms, say what happens next, and sign off from the firm. Keep it under 200 words and do not invent dates, prices, or people.Set prompt to You are writing the body of a welcome email to a brand new client of a professional services firm. Write only the body of the email: no subject line, no placeholder brackets such as [Name], and nothing you cannot support from the details below. Greet the client by name, refer to what they asked for in their own terms, say what happens next, and sign off from the firm. Keep it under 200 words and do not invent dates, prices, or people.
About the firm:
firmName
firmContext
The new client:
Name: clientName
What they told us:
clientNotes.prompt. Save it as written.Ask Claude: prompt. Save it as written.
Maximum response length: 700written.response_text and save it as welcomeBody.Trim written.response_text and save it as welcomeBody.welcomeBody isn't emptyIf welcomeBody isn't emptysubject to Welcome to firmName.Set subject to Welcome to firmName.welcomedTabName in intakeSheetUrl. Save it as welcomedNow.Read column 'A' of welcomedTabName in intakeSheetUrl. Save it as welcomedNow.welcomedNow.values contains emailKeyIf welcomedNow.values contains emailKeyclientEmail is already waiting on another run's approval - leaving rowLabel to it.Show in the output: clientEmail is already waiting on another run's approval - leaving rowLabel to it.welcomedTabName in intakeSheetUrl.Add a row to welcomedTabName in intakeSheetUrl.emailKey to the end of welcomedKeys and save it as welcomedKeys.Add emailKey to the end of welcomedKeys and save it as welcomedKeys.welcomedThisRun.Add 1 to welcomedThisRun.clientName <clientEmail>? Save it as review.Request approval: Send this welcome email to clientName <clientEmail>? Save it as review.Approve and it goes to the client now, under your name. Reject it, or leave it, and it is saved as a Gmail draft for you to edit.
Subject: subject
welcomeBodyoutcome to blank.Set outcome to blank.review.approved is yesIf review.approved is yessendErrorIf a step below fails, save the error as sendErrorclientEmail.Send an email to clientEmail.
Subject: subjectwelcomeBodyoutcome to Sent.Change outcome to Sent.outcome to Send failed.Change outcome to Send failed.draftAfterSendErrorIf a step below fails, save the error as draftAfterSendErrorclientEmail.Draft an email to clientEmail.
Subject: subjectwelcomeBodyoutcome to Send failed, saved as a draft.Change outcome to Send failed, saved as a draft.clientName <clientEmail>: you approved the welcome email but it could not be sent (sendError.message). Recorded as '@outcome' on the '@welcomedTabName' tab. to the end of failures and save it as failures.Add clientName <clientEmail>: you approved the welcome email but it could not be sent (sendError.message). Recorded as '@outcome' on the '@welcomedTabName' tab. to the end of failures and save it as failures.draftErrorIf a step below fails, save the error as draftErrorclientEmail.Draft an email to clientEmail.
Subject: subjectwelcomeBodyoutcome to Drafted.Change outcome to Drafted.outcome to Draft failed.Change outcome to Draft failed.clientName <clientEmail>: the welcome email could not be saved as a draft (draftError.message). Recorded as 'Draft failed' on the '@welcomedTabName' tab. to the end of failures and save it as failures.Add clientName <clientEmail>: the welcome email could not be saved as a draft (draftError.message). Recorded as 'Draft failed' on the '@welcomedTabName' tab. to the end of failures and save it as failures.recordErrorIf a step below fails, save the error as recordErrorwelcomedTabName in intakeSheetUrl.Add a row to welcomedTabName in intakeSheetUrl.clientName <clientEmail>: outcome, but that could not be written to the '@welcomedTabName' tab (recordError.message). to the end of failures and save it as failures.Add clientName <clientEmail>: outcome, but that could not be written to the '@welcomedTabName' tab (recordError.message). to the end of failures and save it as failures.clientName, so rowLabel is left for the next run.Show in the output: No welcome email was written for clientName, so rowLabel is left for the next run.clientEmail: welcomeError.message. If they show as 'Awaiting approval' on the '@welcomedTabName' tab, delete that row to have them offered again. to the end of failures and save it as failures.Add Could not welcome clientEmail: welcomeError.message. If they show as 'Awaiting approval' on the '@welcomedTabName' tab, delete that row to have them offered again. to the end of failures and save it as failures.limitNote to blank.Set limitNote to blank.stoppedAtLimit is yesIf stoppedAtLimit is yesThis run asked about @maxClientsPerRun clients, the most 'maxClientsPerRun' allows; anyone else is offered on the next run. and save it as limitNote.Build the text This run asked about @maxClientsPerRun clients, the most 'maxClientsPerRun' allows; anyone else is offered on the next run. and save it as limitNote.failures isn't emptyIf failures isn't emptyfailures with a new line and save it as failureList.Join failures with a new line and save it as failureList.failureList@limitNote.welcomedThisRun@limitNote.Show in the output: Welcome emails handled this run: welcomedThisRun@limitNote.