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 →
Your contacts live in a Google Sheet that you own and control. Nothing is stored on our servers.
Contacts (capital C, no spaces).Contacts or the app will not be able to read your data.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.
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);
}
Deploying the script as a Web App gives Network Momentum a secure URL to communicate with your sheet.
Now connect Network Momentum to your sheet and add your AI API key to unlock AI-powered suggestions.
"Could not connect" error:
/exec at the end.No contacts loading:
Contacts with a capital C.AI features not working:
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.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.Need help? rebeca@plainspeakingcomms.com
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.
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:
doGet).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.
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:
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:
You're all set. Open the app and add your first contact.
Launch Network Momentum →