Google Sheets + n8n: upsert строки по телефону без дублей ¶
Обновлено: 2026-05-30
Используйте готовый JSON workflow как основу: после импорта замените Google credential, spreadsheetId, название листа и проверьте правило нормализации телефона на своих номерах.
Задача workflow — не просто добавить лид в таблицу, а сохранить одну актуальную строку на один телефон. Повторная заявка из формы, Telegram, VK, CRM или лендинга должна обновить существующую запись, а не создать дубль. Так менеджер видит чистую таблицу, а последующие автоматизации не отправляют повторные сообщения одному и тому же человеку.
Когда нужен upsert, а не append ¶
Append добавляет новую строку при каждом событии. Это удобно для журнала событий, но плохо для таблицы лидов: один и тот же клиент может появиться три раза, фильтры начинают врать, а интеграция с CRM получает неочевидные повторы.
Upsert работает иначе: workflow использует стабильный ключ, ищет существующую запись и либо обновляет её, либо добавляет новую строку. В российских продажах таким ключом часто удобнее делать телефон: email может быть разным, а номер нужен для звонка, WhatsApp, Telegram и CRM.
Архитектура workflow ¶
| Нода | Роль | Что проверить |
|---|---|---|
| Webhook input | принимает lead payload | метод POST, production URL, JSON body |
| Normalize phone | создаёт единый ключ | +7, 8, пробелы, скобки, пустые значения |
| Append or update lead row | пишет в Google Sheets | matching column: phone_normalized |
| Respond to Webhook | возвращает технический ответ | без токенов, лишних персональных данных и сырого payload |
Базовый вариант использует встроенную операцию Google Sheets Append or Update Row. Ручная схема Find row → IF → Update/Append нужна, когда у вас есть сложные ветки: например, не обновлять источник первого лида, писать audit trail, отправлять alert при конфликте или проверять несколько ключей.
Контракт входных данных ¶
Payload должен быть коротким и стабильным. Обязательный минимум — телефон. Остальные поля нужны для удобства менеджера и последующих интеграций: имя, email, источник, внешний id и время получения события.
{
"phone": "+7 (999) 111-22-33",
"name": "Сергей",
"email": "sergey@example.ru",
"source": "vk_lead_form",
"external_id": "lead-58391",
"received_at": "2026-05-30T10:00:00Z"
}
Если телефон пустой или не приводится к нормальному виду, workflow должен остановиться до записи. Не подставляйте фиктивные значения вроде unknown: такие строки потом невозможно дедуплицировать и опасно отправлять в CRM.
Нормализация телефона в Code node ¶
Ключевой шаг — получить одинаковое значение из форматов +7 (999) 111-22-33, 8 999 111 22 33 и 9991112233. Для российской базы лидов удобно хранить ключ в формате +79991112233.
const input = $json.body ?? $json;
const raw = String(input.phone ?? '').trim();
let digits = raw.replace(/\D/g, '');
if (digits.length === 11 && digits.startsWith('8')) {
digits = `7${digits.slice(1)}`;
}
if (digits.length === 10) {
digits = `7${digits}`;
}
if (!/^7\d{10}$/.test(digits)) {
throw new Error(`Invalid phone: ${raw}`);
}
return [{
json: {
phone_raw: raw,
phone_normalized: `+${digits}`,
name: input.name ?? '',
email: input.email ?? '',
source: input.source ?? 'unknown',
external_id: input.external_id ?? input.lead_id ?? '',
first_seen_at: input.received_at ?? new Date().toISOString(),
updated_at: new Date().toISOString(),
update_count: Number(input.update_count ?? 0) + 1
}
}];
Эта нода возвращает phone_raw для аудита и phone_normalized для поиска. В Google Sheets именно phone_normalized должен быть выбран как matching column. Если вы работаете с международными номерами, замените правило на библиотеку или отдельный справочник стран, а не расширяйте регулярку хаотично.
Импортируемый workflow JSON ¶
Полный JSON лежит в архиве и доступен по кнопке «Скачать workflow JSON». Ниже — сокращённая структура, чтобы сразу было видно, что это именно workflow n8n, а не только пример входного payload.
{
"name": "Nodbot - n8n Google Sheets upsert by phone",
"nodes": [
{
"name": "Webhook input",
"type": "n8n-nodes-base.webhook",
"parameters": {
"httpMethod": "POST",
"path": "google-sheets-upsert-by-phone",
"responseMode": "responseNode"
}
},
{
"name": "Normalize phone",
"type": "n8n-nodes-base.code",
"parameters": {
"jsCode": "// full code is shown above in the article"
}
},
{
"name": "Append or update lead row",
"type": "n8n-nodes-base.googleSheets",
"parameters": {
"operation": "appendOrUpdate",
"documentId": "YOUR_SPREADSHEET_ID",
"sheetName": "Leads",
"matchingColumns": [
"phone_normalized"
]
}
},
{
"name": "Respond to Webhook",
"type": "n8n-nodes-base.respondToWebhook"
}
],
"connections": {
"Webhook input": "Normalize phone → Append or update lead row → Respond to Webhook"
}
}
После импорта откройте ноду Google Sheets, выберите credential, укажите spreadsheetId и лист Leads, затем обновите схему колонок. Если n8n не подтянул новые заголовки после изменения таблицы, заново выберите mapping mode и пересохраните ноду.
Настройка Google Sheets ¶
- Создайте отдельный production-лист, а не используйте тестовую таблицу с демо-данными.
- Добавьте колонки
phone_raw,phone_normalized,name,email,source,external_id,first_seen_at,updated_atиupdate_count. - Выдайте Google credential доступ только к нужной таблице, а не ко всему Drive аккаунта.
- Не переименовывайте лист вручную после публикации workflow: лучше сначала изменить ноду, затем таблицу.
Тесты на дубли ¶
Один успешный запуск не доказывает готовность. Отправьте payload дважды: первый запуск должен создать строку, второй — обновить её по phone_normalized. Затем измените имя при том же телефоне и убедитесь, что новая строка не появилась.
curl -X POST "https://YOUR-N8N-DOMAIN/webhook/google-sheets-upsert-by-phone" \
-H "Content-Type: application/json" \
--data @google-sheets-upsert-by-phone-payload.json
Дополнительно проверьте номер в формате 8 999 111 22 33, пустой телефон, лишние пробелы и повторный external_id. Если тесты создают дубли, сначала смотрите matching column, потом нормализацию, затем права Google credential.
Production-риски ¶
- Гонки при одновременных заявках. Если два события с одним телефоном пришли почти одновременно, оба могут успеть создать строку. Для критичных сценариев используйте Webhook + Postgres idempotency.
- Лимиты Google API. Не запускайте массовый импорт через тот же workflow без batch-логики, retry и backoff.
- Ручные правки таблицы. Если менеджер удалил колонку или переименовал лист, нода начнёт падать или писать не туда.
- Персональные данные в executions. Ограничьте сохранение execution data и не логируйте полный payload без необходимости.
- Смешение журнала и карточки лида. Для истории событий заведите отдельный append-only лист, а этот workflow оставьте для актуального состояния клиента.
Критерий готовности ¶
Workflow можно включать в production, когда повторный payload обновляет строку, номер нормализуется одинаково, ошибки Google Sheets попадают в alert, таблица имеет владельца, а команда знает, где менять mapping. Для более строгой защиты от повторов используйте Postgres-идемпотентность, а для обработки временных ошибок — retry и dead-letter queue.