Windows Setup Guide

Get up and running on Windows.

Follow these four steps to connect Network Momentum to your Google Sheet. You only need to do this once. Chrome is recommended.

1
Create your sheet
2
Add the script
3
Deploy the web app
4
Connect and go
5
Calendar (optional)
6
Stale alerts (optional)
7
Weekly digest (optional)
1
Step one

Create Your Google Sheet

Network Momentum stores your contacts in a Google Sheet that you own. Your data never leaves your Google account.

  • 1
    Open Chrome and go to sheets.google.com. Sign in with your Google account if prompted.
  • 2
    Click the large + button (Blank spreadsheet) to create a new empty spreadsheet.
  • 3
    At the bottom of the screen you will see a tab labeled Sheet1. Right-click it and select Rename. Type exactly:
Contacts
Important: The tab name must be exactly Contacts with a capital C. If it is spelled differently, the app will not work.
2
Step two

Add the Apps Script

Google Apps Script is a free tool built into Google Sheets. Paste the script below exactly as written do not change anything.

  • 1
    In your Google Sheet, click Extensions in the top menu, then click Apps Script. A new browser tab will open.
  • 2
    Click inside the code editor. Press Ctrl+A to select all existing code, then press Delete to clear it.
  • 3
    Copy all the code below using the Copy code button, then paste it into the Apps Script editor with Ctrl+V.
  • 4
    Press Ctrl+S to save. The file name should show as Code.gs.
Code.gs Copy and paste all of this
function doGet(e) { return handleRequest(e); }

function doPost(e) {
  var p = JSON.parse(e.postData.contents);
  return handleAction(p);
}

function handleRequest(e) {
  var p = e.parameter;
  return handleAction(p);
}

function authorizeCalendar() {
  CalendarApp.getDefaultCalendar();
}

function installStaleFollowUpTrigger() {
  var triggers = ScriptApp.getProjectTriggers();
  for (var i = 0; i < triggers.length; i++) {
    if (triggers[i].getHandlerFunction() === "checkStaleFollowUps") {
      ScriptApp.deleteTrigger(triggers[i]);
    }
  }
  ScriptApp.newTrigger("checkStaleFollowUps")
    .timeBased()
    .everyDays(1)
    .atHour(6)
    .create();
}

function checkStaleFollowUps() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName("Contacts");
  var rows = sheet.getDataRange().getValues();
  var staleDays = 7;
  var now = new Date();
  var stale = [];

  for (var i = 1; i < rows.length; i++) {
    var row = rows[i];
    var name = row[1];
    var company = row[2];
    var status = row[4];
    var addedAt = row[7];
    var tags = row[9] || "";

    if (status !== "Follow up Email") continue;

    var changedDate = addedAt;
    var match = String(tags).match(/statuschanged:([0-9]{4}-[0-9]{2}-[0-9]{2})/);
    if (match) changedDate = match[1];
    if (!changedDate) continue;

    var days = Math.floor((now - new Date(changedDate)) / 86400000);
    if (days >= staleDays) {
      stale.push({ name: name, company: company, days: days });
    }
  }

  if (stale.length === 0) return;

  var lines = stale.map(function(c) {
    return "- " + c.name + (c.company ? " (" + c.company + ")" : "") +
      " \u2014 " + c.days + " days in Follow up Email";
  });

  var body = "These contacts have been sitting in \"Follow up Email\" status for 7+ days:\n\n" +
    lines.join("\n") +
    "\n\nOpen Network Momentum: https://networkmomentum.app";

  MailApp.sendEmail(
    Session.getActiveUser().getEmail(),
    "Network Momentum: " + stale.length + " stale follow-up" + (stale.length > 1 ? "s" : ""),
    body
  );
}

function installWeeklyDigestTrigger() {
  var triggers = ScriptApp.getProjectTriggers();
  for (var i = 0; i < triggers.length; i++) {
    if (triggers[i].getHandlerFunction() === "sendWeeklyDigest") {
      ScriptApp.deleteTrigger(triggers[i]);
    }
  }
  ScriptApp.newTrigger("sendWeeklyDigest")
    .timeBased()
    .onWeekDay(ScriptApp.WeekDay.MONDAY)
    .atHour(7)
    .create();
}

function sendWeeklyDigest() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName("Contacts");
  var rows = sheet.getDataRange().getValues();
  var now = new Date();
  var weekAgo = new Date(now.getTime() - 7 * 86400000);

  var newThisWeek = [];
  var overdue = [];
  var stuckFollowUp = [];

  for (var i = 1; i < rows.length; i++) {
    var row = rows[i];
    var name = row[1];
    var company = row[2];
    var status = row[4];
    var lastContact = row[5];
    var addedAt = row[7];
    var tags = row[9] || "";

    if (addedAt) {
      var addedDate = new Date(addedAt);
      if (addedDate >= weekAgo) {
        newThisWeek.push({ name: name, company: company });
      }
    }

    if (lastContact && status !== "Closed" && status !== "Follow-up needed" && status !== "Not contacted") {
      var daysSinceContact = Math.floor((now - new Date(lastContact)) / 86400000);
      if (daysSinceContact >= 7) {
        overdue.push({ name: name, company: company, days: daysSinceContact });
      }
    }

    if (status === "Follow up Email") {
      var changedDate = addedAt;
      var match = String(tags).match(/statuschanged:([0-9]{4}-[0-9]{2}-[0-9]{2})/);
      if (match) changedDate = match[1];
      if (changedDate) {
        var daysInStatus = Math.floor((now - new Date(changedDate)) / 86400000);
        if (daysInStatus >= 5) {
          stuckFollowUp.push({ name: name, company: company, days: daysInStatus });
        }
      }
    }
  }

  if (newThisWeek.length === 0 && overdue.length === 0 && stuckFollowUp.length === 0) return;

  var body = "Your Network Momentum weekly digest:\n";

  body += "\nNew contacts this week (" + newThisWeek.length + "):\n";
  if (newThisWeek.length === 0) {
    body += "- None\n";
  } else {
    newThisWeek.forEach(function(c) {
      body += "- " + c.name + (c.company ? " (" + c.company + ")" : "") + "\n";
    });
  }

  body += "\nOverdue, 7+ days since last contact (" + overdue.length + "):\n";
  if (overdue.length === 0) {
    body += "- None\n";
  } else {
    overdue.forEach(function(c) {
      body += "- " + c.name + (c.company ? " (" + c.company + ")" : "") + " \u2014 " + c.days + "d\n";
    });
  }

  body += "\nStuck in Follow up Email, 5+ days (" + stuckFollowUp.length + "):\n";
  if (stuckFollowUp.length === 0) {
    body += "- None\n";
  } else {
    stuckFollowUp.forEach(function(c) {
      body += "- " + c.name + (c.company ? " (" + c.company + ")" : "") + " \u2014 " + c.days + "d\n";
    });
  }

  body += "\nOpen Network Momentum: https://networkmomentum.app";

  MailApp.sendEmail(
    Session.getActiveUser().getEmail(),
    "Network Momentum weekly digest",
    body
  );
}

function handleAction(p) {
  var out = {};

  if (p.action === "headers") {
    out = { headers: ["ID","Name","Company","Role","Status",
      "LastContact","Notes","AddedAt","Priority","Tags",
      "LinkedIn","NotesHistory","Email"] };

  } else if (p.action === "read") {
    var ss = SpreadsheetApp.getActiveSpreadsheet();
    var sheet = ss.getSheetByName("Contacts");
    var rows = sheet.getDataRange().getValues();
    out = { rows: rows };

  } else if (p.action === "append") {
    var ss = SpreadsheetApp.getActiveSpreadsheet();
    var sheet = ss.getSheetByName("Contacts");
    var row = JSON.parse(p.row);
    sheet.appendRow(row);
    out = { success: true };

  } else if (p.action === "append_batch") {
    var ss = SpreadsheetApp.getActiveSpreadsheet();
    var sheet = ss.getSheetByName("Contacts");
    var newRows = JSON.parse(p.rows);
    if (!newRows || !newRows.length) {
      out = { error: "No rows provided" };
    } else {
      var lastRow = sheet.getLastRow();
      sheet.getRange(lastRow + 1, 1, newRows.length, newRows[0].length).setValues(newRows);
      out = { success: true, count: newRows.length };
    }

  } else if (p.action === "update") {
    var ss = SpreadsheetApp.getActiveSpreadsheet();
    var sheet = ss.getSheetByName("Contacts");
    sheet.getRange(parseInt(p.row), parseInt(p.col)).setValue(p.value);
    out = { success: true };

  } else if (p.action === "clear") {
    var ss = SpreadsheetApp.getActiveSpreadsheet();
    var sheet = ss.getSheetByName("Contacts");
    sheet.getRange(parseInt(p.row), 1, 1, 13).clearContent();
    out = { success: true };

  } else if (p.action === "create_event") {
    var title = p.title || "Follow up";
    var description = p.description || "";
    var start = new Date(p.start);
    var minutes = parseInt(p.duration) || 30;
    var end = new Date(start.getTime() + minutes * 60000);
    try {
      var event = CalendarApp.getDefaultCalendar()
        .createEvent(title, start, end, { description: description });
      out = { success: true, eventId: event.getId() };
    } catch (err) {
      out = { error: err.message };
    }

  } else if (p.action === "ai_suggest") {
    var apiKey = p.apiKey;
    var prompt = p.prompt;
    if (!apiKey || !prompt) {
      out = { error: "Missing apiKey or prompt" };
    } else {
      var payload = JSON.stringify({
        model: "claude-sonnet-4-6",
        max_tokens: 1000,
        messages: [{ role: "user", content: prompt }]
      });
      var options = {
        method: "post",
        contentType: "application/json",
        headers: { "x-api-key": apiKey,
          "anthropic-version": "2023-06-01" },
        payload: payload,
        muteHttpExceptions: true
      };
      try {
        var resp = UrlFetchApp.fetch(
          "https://api.anthropic.com/v1/messages", options);
        var result = JSON.parse(resp.getContentText());
        if (result.content && result.content[0]) {
          out = { suggestion: result.content[0].text };
        } else {
          out = { error: "No response from AI" };
        }
      } catch(err) {
        out = { error: err.message };
      }
    }
  } else {
    out = { error: "unknown action" };
  }

  return ContentService
    .createTextOutput(JSON.stringify(out))
    .setMimeType(ContentService.MimeType.JSON);
}
3
Step three

Deploy as a Web App

Deploying the script creates a secure URL that Network Momentum uses to communicate with your Google Sheet.

Note: You will see a security warning during authorization. This is normal. Google shows this for all scripts not listed in the Google Workspace Marketplace. The script only accesses your own Google Sheet.
  • 1
    In the Apps Script editor, click the blue Deploy button in the top right corner.
  • 2
    Select New deployment.
  • 3
    Click the gear icon next to "Select type" and choose Web app.
  • 4
    Set Execute as to Me.
  • 5
    Set Who has access to Anyone.
  • 6
    Click Deploy.
  • 7
    Click Authorize access. Sign in with your Google account.
  • 8
    If you see "Google hasn't verified this app", click Advanced (bottom left), then click "Go to [script name] (unsafe)", then click Allow.
  • 9
    After authorizing, you will see a Web App URL. It looks like: https://script.google.com/macros/s/XXXXXXXXXX/exec. Click Copy next to it. You will need this in the next step.
4
Step four

Connect and Go

Now connect Network Momentum to your Google Sheet.

  • 1
    Go to networkmomentum.app in your browser.
  • 2
    Paste your Web App URL into the Google Apps Script Web App URL field.
  • 3
    Add your AI API key to unlock features like Suggest Next Action and Company Dossier.

    Network Momentum uses Anthropic (Claude) by default. Get an API key at console.anthropic.com. New accounts typically include a small trial credit. After that, usage is billed per token at pay-as-you-go rates. For light personal use, costs stay very low. Check current amounts and pricing at console.anthropic.com. If you prefer OpenAI, get an API key at platform.openai.com. The API is a separate product from Claude.ai and ChatGPT. It is billed per use regardless of any existing subscription. Either way, you can skip this step and add a key later in Settings.
  • 4
    Set your weekly contact goal (5 is a good starting point).
  • 5
    Click Connect my sheet.
Your data is yours. Your API key and Web App URL are stored only in your browser. We never see them.

Troubleshooting

"Could not connect" error:

  • Make sure you copied the full Web App URL including /exec at the end.
  • Make sure deployment settings were: Execute as Me, Who has access Anyone.
  • Try redeploying: Deploy > Manage deployments > pencil icon > New version > Deploy.

No contacts loading:

  • Check that your Google Sheet tab is named exactly Contacts with a capital C.

AI features not working:

  • Anthropic API keys start with sk-ant-. Get one at console.anthropic.com. New accounts typically include a small trial credit. After that, usage is billed per token. Check current amounts at console.anthropic.com. For light personal use, costs stay very low.
  • OpenAI API keys start with sk-. Get one at platform.openai.com. The API is a separate product from Claude.ai and ChatGPT. It is billed per use regardless of any existing subscription.
  • Add or update your key at any time in Settings (top right of the app).

Need help? rebeca@plainspeakingcomms.com

5
Step five · optional

Calendar scheduling

Network Momentum can help you block time for follow-ups two ways: a one-click button that adds an event straight to Google Calendar, or a downloadable calendar file (.ics) that works with macOS Calendar, Outlook, and most other calendar apps.

The .ics download needs no setup at all. It works immediately, for everyone, with no extra authorization. The steps below are only needed if you also want the one-click Google Calendar button.

If you followed Step 2 above using the current code block, the Calendar feature is already included in your script. You just need to grant it permission once:

  • 1
    In the Apps Script editor, find the function dropdown next to the Run button (it probably says doGet).
  • 2
    Click the dropdown and select authorizeCalendar instead.
  • 3
    Click Run. A permissions prompt will appear, click Review permissions, then AdvancedGo to [project name] (unsafe)Allow. This is the same kind of warning you saw during the original setup, it's expected for your own script.
  • 4
    You should see Execution completed in the log with no errors.
  • 5
    Go to Deploy > Manage deployments, click the pencil icon on your existing deployment, set Version to New version, and click Deploy.
Already set up your sheet before this feature existed? Re-copy the full script from Step 2 above into your Apps Script editor (it now includes Calendar support), save, then follow the five steps above.

Seeing an error that says unknown action when you try to schedule something? That means your script doesn't have the Calendar code yet, re-copy Step 2 and try again. An error mentioning permission or authorization means you need to run the authorizeCalendar step above.

6
Step six · optional

Stale follow-up email alerts

Contacts sitting in Follow up Email status get a badge on their card after 3 days, and turn to a Stale badge after 7. If you'd also like a daily email reminder when contacts hit 7+ days, run this one-time setup:

If you followed Step 2 above using the current code block, this feature is already included in your script. You just need to install the daily check once:

  • 1
    In the Apps Script editor, find the function dropdown next to the Run button.
  • 2
    Click the dropdown and select installStaleFollowUpTrigger instead.
  • 3
    Click Run. Approve the permissions prompt the same way you did for Calendar, if asked.
  • 4
    You should see Execution completed in the log with no errors. From now on, the script checks once a day and emails you at your Google account address if anything is stuck.
This only sends an email when something is actually stale. No stale contacts means no email that day.
7
Step seven · optional

Weekly digest email

Want a single weekly summary instead of checking the app? This sends one email every Monday morning covering: new contacts added that week, anyone overdue (7+ days since last contact), and anyone stuck in Follow up Email status (5+ days).

If you followed Step 2 above using the current code block, this feature is already included in your script. You just need to install the weekly check once:

  • 1
    In the Apps Script editor, find the function dropdown next to the Run button.
  • 2
    Click the dropdown and select installWeeklyDigestTrigger instead.
  • 3
    Click Run. Approve the permissions prompt if asked, the same way you did for Calendar.
  • 4
    You should see Execution completed in the log with no errors. From now on, you'll get one email every Monday at 7am summarizing your week.
This runs independently from the daily stale-alert check in Step 6 — you can turn on one, both, or neither.

Ready to build momentum?

You're all set. Open the app and add your first contact.

Launch Network Momentum →