Reads each job application that lands in Gmail, attached CV included, adds the candidate to your Google Sheet and tells your team in Slack.

UsesClaudeGmailGoogle SheetsSlack

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.

GmailGmailWhen an email arrives. Save it as email.When an email arrives. Save it as email.
ValuesSet spreadsheetUrl to blank.Set spreadsheetUrl to blank.
ValuesSet sheetName to Candidates.Set sheetName to Candidates.
ValuesSet slackChannelId to blank.Set slackChannelId to blank.
ValuesSet openRoles to blank.Set openRoles to blank.
ValuesSet 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].
ValuesSet 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].
ValuesSet gmailAccountIndex to 0.Set gmailAccountIndex to 0.
CodeRun Python code on 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.
Decisions & LoopsIf intake.proceed is noIf intake.proceed is no
Yes
Decisions & LoopsFinish the run: intake.reason.Finish the run: intake.reason.
No
No steps
ValuesSet cvFiles to an empty list.Set cvFiles to an empty list.
ValuesSet cvText to an empty list.Set cvText to an empty list.
ValuesSet unreadAttachments to an empty list.Set unreadAttachments to an empty list.
Decisions & LoopsIf intake.message_id isn't blankIf intake.message_id isn't blank
Yes
Decisions & LoopsIf a step below fails, save the error as downloadErrorIf a step below fails, save the error as downloadError
Try
GmailGmailDownload the attachments on email intake. Save it as attachments.Download the attachments on email intake. Save it as attachments.
Decisions & LoopsFor each attachment in attachments.documents:For each attachment in attachments.documents:
Decisions & Loops If attachment.content_type is application/pdf If attachment.content_type is application/pdf
Yes
ListsInsert attachment.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.
Else if ["image/png", "image/jpeg", "image/webp"] contains…
ListsAdd 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.
Else if @attachment.content_type is…
Decisions & LoopsIf a step below fails, save the error as readErrorIf a step below fails, save the error as readError
Try
FilesRead attachment.document_ref Save it as wordFile.Read attachment.document_ref Save it as wordFile.
ListsAdd Add Attached file '@attachment.filename': wordFile.text to the end of cvText and save it as cvText.
On error
ListsAdd 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.
Otherwise
ListsAdd 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.
On error
ListsAdd the attachments (they could not be downloaded: 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.
No
No steps
Decisions & LoopsIf the length of cvFiles is greater than 10If the length of cvFiles is greater than 10
Yes
ListsTake the first 10 items of cvFiles and save it as cvFiles.Take the first 10 items of cvFiles and save it as cvFiles.
No
No steps
TextJoin cvText with nothing and save it as cvTextJoined.Join cvText with nothing and save it as cvTextJoined.
ValuesSet aiAnswer to an empty record.Set aiAnswer to an empty record.
ValuesSet aiProblem to blank.Set aiProblem to blank.
Decisions & LoopsIf a step below fails, save the error as aiErrorIf a step below fails, save the error as aiError
Try
AI AgentRun 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.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.
ValuesChange aiAnswer to screening.output.Change aiAnswer to screening.output.
On error
ValuesChange aiProblem to aiError.message.Change aiProblem to aiError.message.
ValuesSet sheetHeaders to an empty list.Set sheetHeaders to an empty list.
ValuesSet sheetProblem to blank.Set sheetProblem to blank.
Decisions & LoopsIf a step below fails, save the error as headerErrorIf a step below fails, save the error as headerError
Try
Google SheetsGoogle SheetsRead the rows of spreadsheetUrl. Save it as candidatesTab.Read the rows of spreadsheetUrl. Save it as candidatesTab.
ValuesChange sheetHeaders to candidatesTab.headers.Change sheetHeaders to candidatesTab.headers.
On error
ValuesChange sheetProblem to headerError.message.Change sheetProblem to headerError.message.
CodeRun 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.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.
Decisions & LoopsIf candidate.is_application is noIf candidate.is_application is no
Yes
Decisions & LoopsFinish the run: candidate.skip_reason.Finish the run: candidate.skip_reason.
No
No steps
Decisions & LoopsIf sheetProblem is blankIf sheetProblem is blank
Yes
Decisions & LoopsIf candidate.missing_headers isn't emptyIf candidate.missing_headers isn't empty
Yes
TextJoin candidate.missing_headers with , and save it as missingHeaderList.Join candidate.missing_headers with , and save it as missingHeaderList.
TextBuild 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.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.
No
Decisions & LoopsIf a step below fails, save the error as sheetErrorIf a step below fails, save the error as sheetError
Try
Google SheetsGoogle SheetsAdd a row to sheetName in spreadsheetUrl.Add a row to sheetName in spreadsheetUrl.
On error
ValuesChange sheetProblem to sheetError.message.Change sheetProblem to sheetError.message.
No
No steps
ValuesSet slackText to candidate.slack_text.Set slackText to candidate.slack_text.
Decisions & LoopsIf sheetProblem isn't emptyIf sheetProblem isn't empty
Yes
ValuesChange slackText 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 &amp; with < replaced by &lt; with > replaced by &gt;.
No
No steps
Decisions & LoopsIf a step below fails, save the error as slackErrorIf a step below fails, save the error as slackError
Try
SlackSlackSend a message to slackChannelId.Send a message to slackChannelId.slackText
On error
Decisions & LoopsIf sheetProblem isn't emptyIf sheetProblem isn't empty
Yes
Decisions & LoopsStop 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.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.
No
Decisions & LoopsStop 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.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.
Decisions & LoopsIf sheetProblem isn't emptyIf sheetProblem isn't empty
Yes
Decisions & LoopsStop and report a problem: Your team was told in Slack, but candidate.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.
No
No steps
Decisions & LoopsIf candidate.problem isn't emptyIf candidate.problem isn't empty
Yes
ValuesShow in the output: candidate.problem.Show in the output: candidate.problem.
No
No steps
ValuesShow in the output: Added 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.