Setup guide

Get up and running in minutes.

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

Using Windows? We have a step-by-step Windows setup guide with the full script included. View the Windows guide →

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

Your contacts live in a Google Sheet that you own and control. Nothing is stored on our servers.

  • 1
    Go to sheets.google.com and sign in with your Google account.
  • 2
    Click the + button to create a new blank spreadsheet.
  • 3
    At the bottom of the screen, double-click the tab named Sheet1 and rename it exactly: Contacts (capital C, no spaces).
  • 4
    Give your spreadsheet a name at the top, something like Network Momentum Contacts.
Important: The tab must be named exactly Contacts or the app will not be able to read your data.
2
Step two

Add the Apps Script

Google Apps Script acts as the bridge between Network Momentum and your sheet. You'll paste a script that handles reading, writing, and AI suggestions.

  • 1
    In your Google Sheet, click Extensions in the top menu, then Apps Script.
  • 2
    A new tab will open. Delete all the existing code in the editor (select all, then delete).
  • 3
    Copy the full script below and paste it into the editor.
  • 4
    Click the Save icon (or press Cmd+S).
Code.gs paste this entire block
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 as a Web App gives Network Momentum a secure URL to communicate with your sheet.

  • 1
    In the Apps Script editor, click the blue Deploy button at the top right.
  • 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 and authorize the app when prompted.
  • 7
    Copy the Web App URL that appears you'll need it in the next step.
Note: Any time you make changes to the script, you must create a New deployment (not update the existing one) for the changes to take effect.
4
Step four

Connect and go

Now connect Network Momentum to your sheet and add your AI API key to unlock AI-powered suggestions.

  • 1
    Open Network Momentum and paste your Web App URL into the setup screen.
  • 2
    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.
  • 3
    Set your weekly contact goal (5 is a solid starting point).
  • 4
    Click Connect my sheet and you're in.
Your data is yours. Your API key and script 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 →