Reads each job application that lands in Gmail, attached CV included, adds the candidate to your Google Sheet and tells your team in Slack.
Uses
Claude
Gmail
Google Sheets
Slack
Add every emailed job application to your candidate sheet
When a job application arrives in the connected Gmail inbox, the AI reads the email and the CV attached to it - a PDF, a Word document or a photo - and pulls out the candidate's details: name, email, phone, location, the role they are applying for, current title and employer, years of experience, key skills, education, links, availability and a two-sentence summary. Those become one row in your Google Sheet, and a short card in your Slack channel tells the team who applied, with links to the email and the sheet.
Only emails whose subject mentions an application, a CV, a job or a similar word are read, and the AI skips anything that is not an application, noting why in the run history; you can also limit it to a job board's address. A detail neither the email nor the CV gives is left empty and listed in the Notes column, never guessed, and the AI is told not to record age, gender, ethnicity or anything unrelated to the work. Each value goes under its own heading, and if a heading is missing nothing is written: the Slack card says the candidate was not added and the run is marked as failed, so no application goes quietly missing.
You need Gmail, Anthropic, Google Sheets and Slack connected, the link to your sheet and your Slack channel's ID. Give the sheet's first row these headings: Received, Name, Email, Phone, Location, Role applied for, Current title, Current employer, Years of experience, Key skills, Education, Links, Availability, Summary, Subject, Gmail link, Notes.
email.When an email arrives. Save it as email.spreadsheetUrl to blank.Set spreadsheetUrl to blank.sheetName to Candidates.Set sheetName to Candidates.slackChannelId to blank.Set slackChannelId to blank.openRoles to blank.Set openRoles to blank.subjectKeywords to [application, applying, apply, cv, resume, résumé, candidate, vacancy, job, position, role].Set subjectKeywords to [application, applying, apply, cv, resume, résumé, candidate, vacancy, job, position, role].ignoreKeywords to [out of office, automatic reply, auto-reply, autoreply, undeliverable, delivery status notification, mail delivery failed].Set ignoreKeywords to [out of office, automatic reply, auto-reply, autoreply, undeliverable, delivery status notification, mail delivery failed].gmailAccountIndex to 0.Set gmailAccountIndex to 0.message, subject_keywords, ignore_keywords, account_index, and open_roles and save it as intake.Run Python code on message, subject_keywords, ignore_keywords, account_index, and open_roles and save it as intake.intake.proceed is noIf intake.proceed is nointake.reason.Finish the run: intake.reason.cvFiles to an empty list.Set cvFiles to an empty list.cvText to an empty list.Set cvText to an empty list.unreadAttachments to an empty list.Set unreadAttachments to an empty list.intake.message_id isn't blankIf intake.message_id isn't blankdownloadErrorIf a step below fails, save the error as downloadErrorintake. Save it as attachments.Download the attachments on email intake. Save it as attachments.attachment in attachments.documents:For each attachment in attachments.documents:attachment.content_type is application/pdf If attachment.content_type is application/pdfattachment.document_ref into cvFiles at position 0 and save it as cvFiles.Insert attachment.document_ref into cvFiles at position 0 and save it as cvFiles.attachment.document_ref to the end of cvFiles and save it as cvFiles.Add attachment.document_ref to the end of cvFiles and save it as cvFiles.readErrorIf a step below fails, save the error as readErrorattachment.document_ref Save it as wordFile.Read attachment.document_ref Save it as wordFile.wordFile.text to the end of cvText and save it as cvText.attachment.filename to the end of unreadAttachments and save it as unreadAttachments.Add attachment.filename to the end of unreadAttachments and save it as unreadAttachments.attachment.filename to the end of unreadAttachments and save it as unreadAttachments.Add attachment.filename to the end of unreadAttachments and save it as unreadAttachments.downloadError.message) to the end of unreadAttachments and save it as unreadAttachments.Add the attachments (they could not be downloaded: downloadError.message) to the end of unreadAttachments and save it as unreadAttachments.cvFiles is greater than 10If the length of cvFiles is greater than 10cvFiles and save it as cvFiles.Take the first 10 items of cvFiles and save it as cvFiles.cvText with nothing and save it as cvTextJoined.Join cvText with nothing and save it as cvTextJoined.aiAnswer to an empty record.Set aiAnswer to an empty record.aiProblem to blank.Set aiProblem to blank.aiErrorIf a step below fails, save the error as aiErrorscreening.Run an AI agent: Read this email and any CV attached to it, and fill in the candidate's details.@intake.ro… and save it as screening.aiAnswer to screening.output.Change aiAnswer to screening.output.aiProblem to aiError.message.Change aiProblem to aiError.message.sheetHeaders to an empty list.Set sheetHeaders to an empty list.sheetProblem to blank.Set sheetProblem to blank.headerErrorIf a step below fails, save the error as headerErrorspreadsheetUrl. Save it as candidatesTab.Read the rows of spreadsheetUrl. Save it as candidatesTab.sheetHeaders to candidatesTab.headers.Change sheetHeaders to candidatesTab.headers.sheetProblem to headerError.message.Change sheetProblem to headerError.message.reply, ai_problem, intake, received_at, sheet_url, sheet_name, sheet_headers, unread_attachments, and files_read and save it as candidate.Run Python code on reply, ai_problem, intake, received_at, sheet_url, sheet_name, sheet_headers, unread_attachments, and files_read and save it as candidate.candidate.is_application is noIf candidate.is_application is nocandidate.skip_reason.Finish the run: candidate.skip_reason.sheetProblem is blankIf sheetProblem is blankcandidate.missing_headers isn't emptyIf candidate.missing_headers isn't emptycandidate.missing_headers with , and save it as missingHeaderList.Join candidate.missing_headers with , and save it as missingHeaderList.the first row of the '@sheetName' tab is missing these headings: @missingHeaderList. Nothing was written, so no value landed in the wrong column. and save it as sheetProblem.Build the text the first row of the '@sheetName' tab is missing these headings: @missingHeaderList. Nothing was written, so no value landed in the wrong column. and save it as sheetProblem.sheetErrorIf a step below fails, save the error as sheetErrorsheetName in spreadsheetUrl.Add a row to sheetName in spreadsheetUrl.sheetProblem to sheetError.message.Change sheetProblem to sheetError.message.slackText to candidate.slack_text.Set slackText to candidate.slack_text.sheetProblem isn't emptyIf sheetProblem isn't emptyslackText to slackTextChange slackText to slackText
:warning: This candidate was NOT added to the sheet, so please add them by hand from the email. Reason: sheetProblem with & replaced by & with < replaced by < with > replaced by >.slackErrorIf a step below fails, save the error as slackErrorslackChannelId.Send a message to slackChannelId.slackTextsheetProblem isn't emptyIf sheetProblem isn't emptycandidate.name: the row could not be added to '@sheetName' and the Slack message could not be posted, so nobody has been told. Reason: sheetProblem Slack said: slackError.message The application is still in Gmail: intake.link.Stop and report a problem: Nothing was recorded for candidate.name: the row could not be added to '@sheetName' and the Slack message could not be posted, so nobody has been told. Reason: sheetProblem Slack said: slackError.message The application is still in Gmail: intake.link.candidate.name to '@sheetName', but the Slack message could not be posted, so nobody has been told about this application yet. Slack said: slackError.message.Stop and report a problem: Added candidate.name to '@sheetName', but the Slack message could not be posted, so nobody has been told about this application yet. Slack said: slackError.message.sheetProblem isn't emptyIf sheetProblem isn't emptycandidate.name was not added to '@sheetName'. Reason: sheetProblem.Stop and report a problem: Your team was told in Slack, but candidate.name was not added to '@sheetName'. Reason: sheetProblem.candidate.problem isn't emptyIf candidate.problem isn't emptycandidate.problem.Show in the output: candidate.problem.candidate.name to '@sheetName' and told the team in Slack.Show in the output: Added candidate.name to '@sheetName' and told the team in Slack.