<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Dovetail Scripts]]></title><description><![CDATA[Dovetail Scripts]]></description><link>https://dovetailscripts.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sun, 01 Nov 2026 08:13:27 GMT</lastBuildDate><atom:link href="https://dovetailscripts.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Collect leads in a Google Sheet with a free Apps Script webhook (no Zapier)]]></title><description><![CDATA[Zapier and Make are fine tools, but paying a monthly fee to move a form submission into a spreadsheet stings when you have ten leads a month. A Google Apps…]]></description><link>https://dovetailscripts.hashnode.dev/collect-leads-in-a-google-sheet-with-a-free-apps-script-webhook-no-zapier</link><guid isPermaLink="true">https://dovetailscripts.hashnode.dev/collect-leads-in-a-google-sheet-with-a-free-apps-script-webhook-no-zapier</guid><category><![CDATA[automation]]></category><category><![CDATA[#googlesheets]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[webdev]]></category><dc:creator><![CDATA[Dovetail Scripts]]></dc:creator><pubDate>Sat, 31 Oct 2026 04:24:13 GMT</pubDate><content:encoded><![CDATA[<p>Zapier and Make are fine tools, but paying a monthly fee to move a form submission into a spreadsheet stings when you have ten leads a month. A Google Apps Script web app gives you a free HTTPS endpoint that writes submissions straight into a Google Sheet. Here is the whole thing.</p>
<h2>The endpoint</h2>
<p>Open a sheet, go to Extensions &gt; Apps Script, and paste:</p>
<pre><code class="language-javascript">const SECRET = 'change-me';

function doPost(e) {
  const p = e.parameter;
  if (p.token !== SECRET) {
    return json({ ok: false, error: 'bad token' });
  }
  const email = String(p.email || '').trim();
  if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
    return json({ ok: false, error: 'bad email' });
  }
  SpreadsheetApp.getActiveSheet()
    .appendRow([new Date(), String(p.name || ''), email, '', '']);
  return json({ ok: true });
}

function json(obj) {
  return ContentService.createTextOutput(JSON.stringify(obj))
    .setMimeType(ContentService.MimeType.JSON);
}
</code></pre>
<p>Then deploy it: Deploy &gt; New deployment &gt; type "Web app" &gt; Execute as "Me" &gt; Who has access "Anyone". Copy the URL ending in <code>/exec</code>.</p>
<h2>The form</h2>
<p>Any HTML form can post to it:</p>
<pre><code class="language-html">&lt;form action="YOUR_EXEC_URL" method="POST"&gt;
  &lt;input name="name" placeholder="Your name"&gt;
  &lt;input name="email" type="email" required placeholder="you@company.com"&gt;
  &lt;input type="hidden" name="token" value="change-me"&gt;
  &lt;button type="submit"&gt;Send&lt;/button&gt;
&lt;/form&gt;
</code></pre>
<p>Each submission lands as a row: timestamp, name, email, plus two empty columns (<code>status</code>, <code>sent_at</code>) that an auto-reply script can use later.</p>
<h2>Honest caveats</h2>
<ul>
<li>The token in a public page is visible in the source. It stops drive-by bots, not a determined human. For anything sensitive, add real spam protection (a honeypot field helps too).</li>
<li>"Anyone" access means anyone with the URL can post. That is the point of a lead form, but do not reuse the same script for private data.</li>
<li>Apps Script caps concurrent executions and daily runtime. For ordinary lead volumes you will never notice; for a launch-day spike, test first.</li>
</ul>
<h2>Where this goes next</h2>
<p>Once leads land in the sheet, the auto-reply script from <a href="https://dovetailscripts.hashnode.dev/a-25-line-apps-script-that-replies-to-new-leads-from-a-google-sheet">my earlier post</a> answers them within minutes, and the <a href="https://dovetailscripts.hashnode.dev/one-polite-follow-up-when-a-lead-goes-quiet-apps-script">follow-up script</a> bumps the quiet ones after 48 hours.</p>
<p>All three pieces - the webhook, the auto-reply, and the follow-up - are packaged with editable email templates and a 10-minute setup guide in the <a href="https://dovetailscripts.gumroad.com/l/bjslz">LeadReply Kit</a>. The kit is mock-tested sample code, so read it before pointing it at real leads.</p>
]]></content:encoded></item><item><title><![CDATA[One polite follow-up when a lead goes quiet (Apps Script)]]></title><description><![CDATA[A fast first reply wins leads, but plenty of good leads still go quiet. One short follow-up, sent about 48 hours later, recovers a surprising number of…]]></description><link>https://dovetailscripts.hashnode.dev/one-polite-follow-up-when-a-lead-goes-quiet-apps-script</link><guid isPermaLink="true">https://dovetailscripts.hashnode.dev/one-polite-follow-up-when-a-lead-goes-quiet-apps-script</guid><category><![CDATA[automation]]></category><category><![CDATA[#googlesheets]]></category><category><![CDATA[gmail]]></category><category><![CDATA[JavaScript]]></category><dc:creator><![CDATA[Dovetail Scripts]]></dc:creator><pubDate>Fri, 30 Oct 2026 04:23:01 GMT</pubDate><content:encoded><![CDATA[<p>A fast first reply wins leads, but plenty of good leads still go quiet. One short follow-up, sent about 48 hours later, recovers a surprising number of them. More than one starts to feel like pestering.</p>
<p>Here is a small Google Apps Script that does exactly that from the same Google Sheet you use to track leads. It pairs with the auto-reply script from <a href="https://dovetailscripts.hashnode.dev/a-25-line-apps-script-that-replies-to-new-leads-from-a-google-sheet">my earlier post</a>, but it also works on its own.</p>
<h2>The sheet layout</h2>
<p>Same columns as before: <code>email</code>, <code>status</code>, <code>sent_at</code>. The first script sets <code>status</code> to <code>sent</code> and stamps <code>sent_at</code> when it replies. This one looks for rows that are still <code>sent</code> (never replied to) with a <code>sent_at</code> older than 48 hours.</p>
<h2>How it knows the lead never replied</h2>
<p>It asks Gmail. If any message from the lead's address arrived after your first reply went out, the lead is not quiet, so the script skips them:</p>
<pre><code class="language-javascript">function sendFollowUps() {
  const sheet = SpreadsheetApp.getActiveSheet();
  const rows = sheet.getDataRange().getValues();
  const head = rows[0];
  const col = n =&gt; head.indexOf(n);
  const HOURS = 48;
  const MAX_PER_RUN = 10;
  const now = Date.now();
  let sent = 0;

  for (let i = 1; i &lt; rows.length &amp;&amp; sent &lt; MAX_PER_RUN; i++) {
    if (String(rows[i][col('status')]) !== 'sent') continue;
    const sentAt = new Date(rows[i][col('sent_at')]);
    if (isNaN(sentAt) || (now - sentAt) &lt; HOURS * 3600 * 1000) continue;

    const email = String(rows[i][col('email')]).trim();
    const replied = GmailApp.search(`from:${email} after:${Math.floor(sentAt.getTime() / 1000)}`, 0, 1);
    if (replied.length &gt; 0) {
      sheet.getRange(i + 1, col('status') + 1).setValue('replied');
      continue;
    }

    GmailApp.sendEmail(email, 'Still happy to help', 'Hi, just bumping this in case it got buried. Happy to answer any questions - or if now is not the right time, no worries at all.');
    sheet.getRange(i + 1, col('status') + 1).setValue('followed_up');
    sent++;
  }
}
</code></pre>
<h2>What it does, line by line</h2>
<ul>
<li>Only touches rows marked <code>sent</code> with a <code>sent_at</code> at least 48 hours old.</li>
<li>Searches Gmail for anything from the lead after your first reply. A reply flips the row to <code>replied</code> so you never follow up with someone who already wrote back.</li>
<li>Sends one short follow-up, then marks the row <code>followed_up</code> so it never sends a second one.</li>
<li><code>MAX_PER_RUN</code> caps each run. Schedule it with a time-driven trigger (Triggers &gt; Add trigger &gt; hourly) and it stays far under Gmail's daily sending limits.</li>
</ul>
<h2>Two things that matter more than the code</h2>
<p>Keep the follow-up short and human. Two sentences, no pitch, no "just checking in" three times over. And stop after one. A lead who ignores two emails was never going to buy from the third.</p>
<hr />
<p>I packaged this follow-up, the instant auto-reply, editable email templates, and a 10-minute setup guide into the <a href="https://dovetailscripts.gumroad.com/l/bjslz">LeadReply Kit</a> if you would rather skip the wiring. The kit is mock-tested sample code, so read it before pointing it at real leads.</p>
]]></content:encoded></item><item><title><![CDATA[A 25-line Apps Script that replies to new leads from a Google Sheet]]></title><description><![CDATA[Speed matters with leads: a reply within minutes beats one the next day. If your leads land in a Google Sheet (from a form, Zapier, Make, or a Lead Ads…]]></description><link>https://dovetailscripts.hashnode.dev/a-25-line-apps-script-that-replies-to-new-leads-from-a-google-sheet</link><guid isPermaLink="true">https://dovetailscripts.hashnode.dev/a-25-line-apps-script-that-replies-to-new-leads-from-a-google-sheet</guid><category><![CDATA[automation]]></category><dc:creator><![CDATA[Dovetail Scripts]]></dc:creator><pubDate>Tue, 27 Oct 2026 05:20:24 GMT</pubDate><content:encoded><![CDATA[<p>Speed matters with leads: a reply within minutes beats one the next day. If your leads land in a Google Sheet (from a form, Zapier, Make, or a Lead Ads connector), this small Apps Script replies from your own Gmail. No paid tool needed.</p>
<h2>Sheet layout</h2>
<p>Row 1 headers: <code>name</code>, <code>email</code>, <code>status</code>, <code>sent_at</code>. New leads are rows with an empty <code>status</code>.</p>
<h2>The script</h2>
<p>Open Extensions &gt; Apps Script and paste:</p>
<pre><code class="language-javascript">function replyToNewLeads() {
  const sheet = SpreadsheetApp.getActiveSheet();
  const rows = sheet.getDataRange().getValues();
  const head = rows[0];
  const col = n =&gt; head.indexOf(n);
  const MAX_PER_RUN = 20;
  let sent = 0;
  for (let i = 1; i &lt; rows.length &amp;&amp; sent &lt; MAX_PER_RUN; i++) {
    const email = String(rows[i][col('email')]).trim();
    if (rows[i][col('status')]) continue;
    if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
      sheet.getRange(i + 1, col('status') + 1).setValue('invalid');
      continue;
    }
    GmailApp.sendEmail(email, 'Thanks for reaching out',
      'Hi ' + rows[i][col('name')] + ',\n\nThanks for your message. We will be in touch shortly.\n');
    sheet.getRange(i + 1, col('status') + 1).setValue('sent_1');
    sheet.getRange(i + 1, col('sent_at') + 1).setValue(new Date());
    sent++;
  }
}
</code></pre>
<h2>Run it on a timer</h2>
<p>In Apps Script, open Triggers (the clock icon), add a trigger for <code>replyToNewLeads</code>, time-driven, every 5 minutes.</p>
<h2>Things to know</h2>
<ul>
<li>Free Gmail allows about 100 scripted recipients a day, Google Workspace about 1,500.</li>
<li>Only email people who asked to hear from you.</li>
<li>The status column stops duplicates. Do not clear it unless you want a resend.</li>
</ul>
<h2>Want the polished version?</h2>
<p>I packaged this with HTML email templates, a timed follow-up for people who do not reply, a duplicate lock and a setup guide as <a href="https://dovetailscripts.gumroad.com/l/bjslz">LeadReply Kit</a> ($29). The script above works fine on its own, though.</p>
]]></content:encoded></item></channel></rss>