How to Automate Lead Follow-Ups with n8n, Gemini AI, Slack, and Gmail
Install an n8n workflow that captures website form leads, drafts a reply with one AI agent, then runs a Slack approve or rewrite loop before saving a Gmail draft, logged in Google Sheets.
Lead follow-up is a speed problem and a trust problem at the same time. Reply fast and you win the deal, but a generic auto-responder that misreads the question does more harm than silence. Most "AI reply" setups pick one side: instant and careless, or careful and slow.
This guide builds the version that holds both. A lead submits a form, n8n logs the row in Google Sheets, one AI agent drafts a personalized first reply, and the draft goes to Slack with two buttons: Approve and Send or Rewrite. Nothing reaches the lead until a human signs off, and on approval n8n creates a Gmail draft for a final human read. It is the installable build of the form-to-reply playbook.
Available resources#
This build uses two assets you will set up below:
- n8n workflow "Email Draft - For web form submission" (capture, draft, Slack review loop, Gmail draft).
- Google Sheet "Leads" (the system of record and audit log for every lead and its status).
What you'll need#
Before you begin, make sure you have:
- An n8n account (cloud or self-hosted) reachable at a public HTTPS webhook URL.
- A Slack workspace and a Slack app with a bot token, plus Interactivity enabled.
- A Google account with one Google Sheet used as the system of record.
- A Gmail account for draft creation.
- An LLM API key for the agent (any chat model works; this build uses Google Gemini).
Overview of the automation#
The automation runs in two phases that share one Google Sheet. A human approval sits between them.
- Draft creation. A form submission logs the lead to Google Sheets, one AI agent drafts a personalized first reply, and the draft is posted to Slack with Approve and Rewrite buttons.
- Review and send. Approving the draft creates a Gmail draft. Choosing Rewrite opens a Slack modal for feedback, the same agent rewrites the email, and the new version is reposted with the same buttons. This loops until someone approves.
One agent does both jobs (first draft and rewrites), so there is only a single prompt and model to maintain. A small Switch reads where the run started and sends the agent's output to the right place.
Capture Name, Email, and Message from the lead.
Write one row per lead: name, email, message, created_at.
One agent drafts the first reply, and later rewrites it from Slack feedback.
Send a fresh draft to Slack, or a rewrite back to the same Slack thread.
Post the draft with Approve and Rewrite buttons.
Route the click: approve, or open a feedback modal to rewrite.
- Gmail draft created on approve
The important design decision is the approval gate before anything leaves the building. Drafting is cheap and reversible. An email to a prospect is neither. Keeping the Gmail draft behind a human click means a misread inquiry dies quietly in Slack instead of landing in a lead's inbox.
Step-by-step setup#
1. Set up the Google Sheet#
Create one Google Sheet (the example names it "Leads") with these columns. Created_At doubles as the stable key you match on for every later update.
| Column | Purpose |
|---|---|
Lead_Name | Lead display name from the form |
Lead_Email | Reply-to address for the Gmail draft |
Lead_Message | The free-text message the lead submitted |
Created_At | Timestamp, also used as the row match key |
AI_Draft | The latest draft text for audit |
Message_Id | Slack message timestamp, used to correlate clicks to a row |
Send_Status | Optional status column for tracking |
2. Connect credentials in n8n#
Add four credentials so the agent, Slack, Sheets, and Gmail nodes can authenticate:
- Google Sheets (OAuth2) with read and write on the Leads sheet.
- Google Gemini for the chat model behind the agent.
- Slack (bot token) with
chat:write, plus Interactivity enabled. - Gmail (OAuth2) for draft creation.
3. Build phase one: capture, draft, and post to Slack#
Capture the lead. Add a Form Trigger named Lead Form Trigger with three fields: Name, Email (email type), and Message. This gives you a hosted form URL you can link from your site, or replace later with a Webhook node if you use Typeform, Tally, or a custom form.
Save the lead to Google Sheets. Add a Google Sheets node named Save Lead to Sheet set to Append. Map Lead_Name, Lead_Email, and Lead_Message from the form, and Created_At from the submission time formatted as yyyy-MM-dd HH:mm:ss.
Draft the reply with one AI agent. Add a LangChain AI Agent named Email Agent with a Google Gemini Chat Model attached. This single agent does both jobs: it writes the first draft now, and later rewrites that draft from reviewer feedback. You do not need a second agent or a memory node, because every call already receives the full context it needs.
Because the same agent handles rewrites, set its input to switch on whether reviewer feedback is present. New leads have no feedback field, so the agent drafts; rewrites carry feedback, so it edits:
={{ $json.feedback
? `Rewrite the previous email applying the reviewer's feedback.\n\nPrevious Email:\n${$json.prev_email}\n\nImprovements requested by the reviewer:\n${$json.feedback}`
: `New inbound inquiry. Write the first-draft reply.\n\nClient Name: ${$json.Lead_Name}\nClient Email: ${$json.Lead_Email}\nClient Message:\n${$json.Lead_Message}` }}Keep one combined system prompt that covers both jobs: greet the client by first name, answer the actual question, sign off with your team name, never emit bracketed placeholders or invented links, and return a fixed format so downstream parsing stays reliable:
Subject: <one-line subject>
<email body>Route the agent output, then post to Slack. Because one agent serves both a fresh draft and later rewrites, send its output through a Switch named Route Agent Output. On a brand-new lead, route to Post Draft to Slack; on a rewrite, route to Repost Rewritten Draft (phase two). The Switch tells them apart by checking which earlier node ran in this execution:
- Draft path:
{{ $('Save Lead to Sheet').isExecuted }}istrue. - Rewrite path:
{{ $('Parse Slack Payload').isExecuted }}istrue.
Use .isExecuted rather than reading the agent's input fields directly, because the agent does not pass those fields through.
Now add a Slack node named Post Draft to Slack using Block Kit. Render the draft in a section block, then an actions block with two buttons:
{
"blocks": [
{
"type": "section",
"text": { "type": "mrkdwn", "text": "{{ JSON.stringify($json.output) }}" }
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": { "type": "plain_text", "text": "Approve & Send" },
"value": "approve",
"action_id": "email_approve"
},
{
"type": "button",
"text": { "type": "plain_text", "text": "Rewrite" },
"value": "rewrite",
"action_id": "email_rewrite"
}
]
}
]
}Save the Slack message id back to the row. Add a Google Sheets node named Save Message ID set to Update, matching on Created_At. Store the Slack message timestamp as Message_Id. This is how a future button click finds the right lead row.
4. Build phase two: the Slack approve and rewrite loop#
Receive Slack interactivity with a webhook. Add a Webhook node named Slack Interaction Webhook (POST, path slack-rdkd). Set it to respond immediately with no body. Slack needs a fast response, and the trigger id used to open a modal expires within a few seconds, so do the slow work after the acknowledgement. In your Slack app settings, enable Interactivity and set the Request URL to your production webhook URL, for example https://your-n8n-domain/webhook/slack-rdkd.
Parse the Slack payload. Add a Code node named Parse Slack Payload. Slack sends two shapes to the same URL: a block_actions event when a button is clicked, and a view_submission event when the modal is submitted. Branch on the type:
const raw = $input.first().json.body.payload;
const payload = typeof raw === "string" ? JSON.parse(raw) : raw;
if (payload.type === "block_actions") {
const action = payload.actions[0];
const fullEmailText = payload.message.blocks[0].text.text;
const lines = fullEmailText.split("\n");
return {
json: {
event_type: action.value, // approve or rewrite
message_ts: payload.message.ts,
trigger_id: payload.trigger_id,
email_subject: lines[0].replace("Subject: ", "").trim(),
email_body: fullEmailText
.substring(fullEmailText.indexOf("\n\n") + 2)
.trim(),
full_email_text: fullEmailText,
},
};
}
if (payload.type === "view_submission") {
const metadata = JSON.parse(payload.view.private_metadata);
return {
json: {
event_type: "view_submission",
feedback: payload.view.state.values.feedback_block.feedback_input.value,
prev_email: metadata.prev_email,
message_ts: metadata.message_ts,
},
};
}
throw new Error(`Unsupported Slack payload type: ${payload.type}`);Route the event. Add a Switch node named Route Slack Event with three outputs based on event_type: approve, rewrite, and view_submission.
Approve branch (Gmail draft). On the approve output:
Lookup Lead (Approve): a Google Sheets read that finds the row byMessage_Id.Save Approved Draft: a Google Sheets update that writes the approved text toAI_Draft.Create Gmail Draft: a Gmail node set to draft, using the parsed subject and body.
The draft is created, not sent, so a human can do a final review in Gmail before it goes out.
Rewrite branch (open a feedback modal). On the rewrite output, add an HTTP Request node named Open Feedback Modal that calls the Slack views.open API with the click's trigger_id. The modal shows a multi-line feedback input and carries the draft plus the message id in private_metadata so the next step has context:
{
"trigger_id": "{{ $json.trigger_id }}",
"view": {
"type": "modal",
"callback_id": "rewrite_modal",
"private_metadata": "{{ JSON.stringify({ message_ts: $json.message_ts, prev_email: $json.full_email_text }) }}",
"title": { "type": "plain_text", "text": "Rewrite Email" },
"submit": { "type": "plain_text", "text": "Submit" },
"blocks": [
{
"type": "section",
"text": { "type": "mrkdwn", "text": "*Current draft:*" }
},
{
"type": "input",
"block_id": "feedback_block",
"label": { "type": "plain_text", "text": "What should we improve?" },
"element": {
"type": "plain_text_input",
"action_id": "feedback_input",
"multiline": true
}
}
]
}
}A free-text input only renders a Submit button inside a modal, which is why the modal is opened with views.open rather than posting an input block into the channel.
Handle the modal submit (rewrite loop). On the view_submission output:
- The same
Email Agentfrom phase one receives the previous email plus the reviewer feedback and returns a new draft in the sameSubject:format. The conditional input makes the one agent edit instead of draft. Route Agent Outputsees this run started fromParse Slack Payload, so it sends the result toRepost Rewritten Draft, a Slack node that posts the new draft with the same Approve and Rewrite buttons.Lookup Lead (Rewrite)andUpdate Message ID (Rewrite): Google Sheets steps that keep the row pointed at the latest Slack message.
Because the reposted message has the same buttons, the reviewer can rewrite again or approve. The loop continues until approval triggers the Gmail draft.
5. Import the workflow JSON#
Grab the ready-made workflow with the download button above, or export your own by opening the workflow in n8n and using the menu to Download the JSON. On the target instance, import the file, then re-create or re-select the four credentials from Step 2. Finally, update the Slack channel id and the Google Sheet document id to match your workspace.
Testing the workflow#
Validate each phase before pointing it at real leads:
- Submit a test lead. Fill out the form with a name, email, and a real question. Confirm a row appears in the Leads sheet and a draft posts to your Slack channel with both buttons.
- Read the draft. Check that the email greets the lead by name, answers the actual question, and contains no bracketed placeholders or invented links.
- Run the rewrite loop. Click Rewrite, type feedback like "make it shorter and add a clear next step", and submit. Confirm a new draft reposts with the same buttons.
- Approve and confirm. Click Approve & Send, then open Gmail and confirm a new draft exists (not a sent email) with the approved subject and body.
If a button click does nothing, check the message id first: if Message_Id was never saved, the lookup returns no row, which is the most common failure point.
Customization options#
- Swap the model. Replace Gemini with OpenAI, Anthropic, or any chat model n8n supports. The agent and prompt stay the same.
- Replace the form. Swap the n8n Form Trigger for a Webhook node fed by Typeform, Tally, or a custom site form, keeping the same Sheets mapping.
- Auto-send on approval. Change the Gmail node from draft to send once you trust the output, or add a delay before sending.
- Route to more reviewers. Post drafts to different Slack channels by lead type, or add a second approver button before the Gmail step.
- Track richer status. Use the
Send_Statuscolumn to recordDrafted,Approved, andSentfor reporting on response times.
Common mistakes that quietly break this#
- Treating Slack as the record. The Google Sheet is the audit log. Read the row, do not rely on Slack history, which is easy to edit or lose.
- Breaking correlation. Every button click is matched back to a row by
Message_Id. If a lookup returns no row, the message id was never saved, so check theSave Message IDstep. - Slow webhook acknowledgement. Keep the webhook responding immediately. If the modal stops opening, the trigger id likely expired because a slow node ran before the acknowledgement.
- Double approvals. A double approve can create two Gmail drafts. Disable the buttons after the first action, or check
Send_Statusbefore creating the draft. - Skipping signature verification. Verify the Slack signing secret on every callback, and keep the review channel private to limit lead PII exposure.
Conclusion#
You now have a lead follow-up engine that replies fast without going rogue. A form submission becomes a researched, on-brand draft in Slack within seconds; a reviewer approves it or rewrites it in a tight loop; and only an approved draft ever lands in Gmail, with every step logged in Google Sheets. Speed when you want it, and a human gate where it counts.
Keep reading
- AI agents & LLMn8nImplementationAI agents & LLM
Automate LinkedIn posts with n8n: AI agents that research, write, and publish on approval
Build an n8n pipeline that researches a topic, drafts a LinkedIn post in two competing styles, generates an image brief, and publishes to LinkedIn only after you approve it in Airtable.
8 min · - AI agents & LLMn8nPlaybookAI agents & LLM
AI form replies with human-in-the-loop: architecture for n8n, Slack, and Google Sheets
Design a form-to-reply pipeline in n8n: capture submissions, draft with an AI agent, route Slack approvals, send email only after sign-off, and track every row in Google Sheets.
7 min · - Business processn8nImplementationBusiness process
How to Automate Customer Review Management with n8n, Gmail, and Gemini AI
Install an n8n workflow that emails review requests, filters unhappy feedback privately, logs ratings in Google Sheets, and drafts Google review replies with Gemini AI.
12 min ·