Tutorials n8n

n8n Google Sheets Tutorial: Credentials, Rows, Daily Digest

Connect n8n to Google Sheets step by step: set up OAuth or a service account in Google Cloud, use Append Row, Update Row, Get Row(s), and Append or Update, build a form-to-sheet workflow and a daily due-items digest, and fix redirect_uri_mismatch, 7-day token expiry, and quota errors.

The RoamerHost Team 22 min read
Ready to start? Managed n8n hosting, set up in 60 seconds Workflow automation platform. From avg. NT$499/mo (billed yearly). Subscribe to n8n
On this page

This guide walks you through connecting n8n to Google Sheets and building two workflows you can actually use: one that adds a new row to a spreadsheet every time a form or webhook receives data, and one that reads the sheet every morning at 8am and sends you a list of items due today. You need a Google account and an n8n 2.x instance with a stable HTTPS URL. Plan on about 40 minutes the first time; most of it goes into the Google Cloud credential setup, and the n8n side is quick.

Based on the n8n 2.43 editor (Google Sheets node version 4.7) and Google's official documentation, verified as of October 2026. Google Cloud console menu names change from time to time, so trust what you see on screen.

If nodes, expressions, and the executions list are still new to you, spend ten minutes on building your first workflow first. If you are still deciding whether n8n is the right tool, start with what n8n is.

Choose a credential type: OAuth or service account

The Google Sheets node supports two authentication methods. Open the node and the Authentication field shows "OAuth2 (recommended)" and "Service Account". Both require a Google Cloud project. The difference is whose identity opens the spreadsheet.

With OAuth, you sign in with your own Google account, and n8n gets the same access you have: any spreadsheet you can see in Drive, n8n can read and write, with no extra sharing. The trade-off is that you have to configure Google's consent screen, and while the app stays in "Testing" status the authorization expires every seven days (more on that below). A service account is a robot account that Google creates inside your project. It has its own email address and only sees files you share with it. It never hits the seven-day expiry, which makes it a good fit for long-running background jobs. Note that the n8n docs warn that service accounts created after April 15, 2025 can no longer access "My Drive" directly, so keeping files in a shared drive (a Google Workspace feature) is the safer setup.

CompareOAuth2Service account
Acts asYour own Google accountA robot account with its own email
Can accessEvery sheet you can openOnly files shared with it
Does access expire?Every 7 days in Testing; not once published or set to InternalNo, as long as the key exists
Best forIndividuals and small teams getting startedWorkspace companies that want workflows decoupled from any employee

If this is your first integration and the sheet lives in a personal Gmail Drive, use OAuth. If your company runs Google Workspace and you want workflows to survive an employee leaving, consider a service account. On n8n Cloud, some Google nodes offer a managed "Sign in with Google" button; self-hosted and hosted instances do not have that shortcut, so the setup below is required.

Option 1: Create OAuth credentials in Google Cloud

You will switch between two tabs: the Google Cloud console (console.cloud.google.com) and the n8n credential dialog. Open the n8n side first, because you need to copy the callback URL from it. Go to Credentials and create a new credential (or choose Create new credential from any Google Sheets node), then pick Google Sheets OAuth2 API. The top of the dialog shows an OAuth Redirect URL. On a RoamerHost instance it looks like this:

https://your-name.roamerhost.com/rest/oauth2-credential/callback

Copy that URL, then work through the Google Cloud console in order:

  1. Create a new project from the project picker in the top-left corner and give it a recognizable name such as n8n-sheets. Make sure the console has switched to it. Under APIs & Services, open the Library, search for Google Sheets API and enable it, then do the same for Google Drive API. The n8n Google Sheets credential needs both; without the Drive API, the node cannot list your spreadsheets.
  2. Configure the consent screen. The menu is now called Google Auth Platform (formerly "OAuth consent screen"). On Overview, click Get started and fill in the App name and User support email. For Audience, pick Internal if you have Google Workspace and only company accounts will use it; personal Gmail accounts can only pick External. Add a contact email, accept the user data policy, and click Create.
  3. On the Branding page, add your n8n URL's root domain under Authorized domains: your own domain if you self-host, or roamerhost.com on RoamerHost. Save.
  4. If you chose External, go to the Audience page, find Test users, click Add users, and add the Gmail address you will authorize with. Any account not on this list is blocked during sign-in.
  5. On the Clients page, create a client (creating an OAuth client ID from APIs & Services > Credentials also works). Set Application type to Web application, click Add URI under Authorized redirect URIs, paste the URL you copied from n8n, and click Create. Google shows the Client ID and Client Secret. The full secret is only shown this once, so copy it or download the JSON right away; if you lose it, you have to generate a new one.
  6. Back in n8n, paste the Client ID and Client Secret and click Sign in with Google. Choose the account you added as a test user. Because the app is not verified by Google, you will see a "Google hasn't verified this app" warning; click Advanced, then continue to the app, and grant access to Sheets and Drive. When n8n shows Account connected, click Save.

The unverified-app warning looks alarming, but it only means you built the app yourself and never submitted it for Google review. The Client ID lives in your own n8n instance, so in practice you are the only one who will ever authorize it.

Deal with the seven-day Testing limit before going live

For an app with an External audience and a Testing publishing status, Google issues refresh tokens that expire after seven days, as stated in Google's OAuth documentation. The symptom is a workflow that runs fine for about a week and then every Google node fails at once. You have three ways out. With Workspace, switch the audience to Internal; internal apps do not have this limit. With a personal account, click Publish app on the Audience page to move the app to In production, and the seven-day expiry no longer applies. You will still see the unverified warning, and an unverified app can be authorized by at most 100 users in total, which is plenty for you and a few teammates. The third option is to stay in Testing and reconnect the credential in n8n every week, which only makes sense while you are still prototyping.

Option 2: Connect with a service account

You still need a project with the Google Sheets API and Google Drive API enabled (step 1 above), but you can skip the consent screen and redirect URL entirely:

  1. Under APIs & Services > Credentials, create a credential of type Service account. Enter a name (the ID fills in automatically), skip the role for now, and click Done.
  2. Open the new service account, go to the Keys tab, add a key, create a new key, choose JSON, and your browser downloads a .json file. Treat this file like a password: keep it out of Git and out of group chats.
  3. In n8n, create a Google Service Account API credential. Set Service Account Email to the client_email value from the JSON file. Paste the private_key value into Private Key, from -----BEGIN PRIVATE KEY----- through -----END PRIVATE KEY-----, without the surrounding double quotes. Leave Region at its default and leave Impersonate a User off.
  4. Open the spreadsheet, click Share, add the client_email address, and give it Editor access. Then set the Google Sheets node's Authentication to Service Account and select this credential.

On Google Workspace, key creation may be blocked by the organization policy iam.disableServiceAccountKeyCreation. Only an organization admin in Google Cloud can grant an exception for your project. Also, service accounts have no Drive storage of their own, so asking one to create a new spreadsheet from the node usually fails. Have a person create the sheet and share it with the service account instead.

The four Google Sheets operations you will use most

Add a Google Sheets node, set Resource to Sheet Within Document, and pick an Operation. Document is the spreadsheet, chosen From list, By URL, or By ID (the long string in the middle of the URL). Sheet is the tab at the bottom of the spreadsheet, with an extra By Name mode. Once both are selected, the node reads the first row of that tab as the header, and every column mapping depends on it.

OperationWhat it doesTypical use
Append RowAdds a row after the last row of dataLog every form entry, order, or message
Update RowFinds a row by matching a column and edits it; skips if not foundChange a status, fill in details
Get Row(s)Reads the whole sheet or only rows that match filtersDaily reports, price lookups
Append or Update RowUpdates the matching row, or appends one if none matchesData that may be sent twice

All three write operations have a Mapping Column Mode. Map Each Column Manually lists every header column so you can type a value or drag a field into each one; it is the clearest and least error-prone choice for beginners. Map Automatically matches incoming field names to header names, and only identical names match. What happens to fields that do not match is controlled by Handling extra fields in input under Options, and the default, Insert in New Column(s), adds a new column at the right edge of the sheet.

Update Row and Append or Update Row add a Column to match on setting, where you pick the column used to find the row, such as Email or an order number. Keep that column unique: when several rows share a value, only the topmost one is updated. If the header has a column named id, the node picks it as the match column automatically. Update Row can also match on row_number, which pairs nicely with Get Row(s), because every row it returns includes a row_number field you can use to update exactly that row.

Get Row(s) lets you add several Filters, each with a Column and the Value it must equal, combined with AND or OR under Combine Filters. Matching is an exact text comparison, so case, stray spaces, and date formats all matter. Current versions return every matching row by default; turn on Return only First Matching Row under Options if you want just one. Output Formatting controls what you get back: numbers come back unformatted by default, while dates come back as the formatted text you see in the sheet. That detail matters for the daily reminder below.

If your header is not on row 1 (say there are two rows of notes above it), set Header Row and First Data Row under Data Location on Sheet, or the node will treat your notes as column names. Write operations also offer Minimise API Calls, which uses Google's append method and saves requests, but the node's own hint says to use it only when the sheet has no gaps between rows or columns; with blank rows in the middle, data can end up misaligned.

Example 1: Write each form submission to a new row

Prepare the sheet first. Create a new Google Sheet, rename the tab to Leads, and type these headers in row 1: Time, Name, Email, Phone, Request, Status. Try not to rename them later; the troubleshooting section explains why.

  1. Create a workflow and add n8n Form Trigger as the first node (search for "form"). Set Form Title to "Book a consultation" and add four Form Elements labeled Name, Email, Phone, and Request, with Element Types Text, Email, Text, and Textarea. Turn on Required Field for Name and Email.
  2. Add a Google Sheets node with Operation set to Append Row, choose your credential, pick the spreadsheet From list, and set Sheet to Leads. Choose Map Each Column Manually, and fill in the six columns as shown below.
  3. Click Execute workflow at the bottom of the canvas. n8n opens the test form (if it does not, copy the Test URL from the form node into your browser). Submit an entry, confirm both nodes show a check mark, and open the sheet to see the new row.
  4. When it works, click Publish at the top of the canvas. n8n 2.x replaced the old Active toggle with publishing: edits are saved as drafts automatically, production URLs only run the published version, and every later change needs another Publish. Share the form's Production URL; the Test URL only works while you are running the workflow from the editor.
Time      {{ $now.toFormat('yyyy-MM-dd HH:mm') }}
Name      {{ $json['Name'] }}
Email     {{ $json['Email'] }}
Phone     {{ $json['Phone'] }}
Request   {{ $json['Request'] }}
Status    New

The Time column uses $now, which follows your n8n time zone. The form node also outputs a submittedAt field, but it is an ISO timestamp with an offset (for example 2026-10-27T14:03:12.000+08:00), which is hard to read in a sheet, so formatting with toFormat is cleaner. Bracket notation such as $json['Name'] is the safest way to reference fields, and it is mandatory when a name contains spaces or hyphens.

To use your own website form instead of the built-in one, replace the first node with a Webhook node set to POST. The JSON your site sends lands under body, so expressions become {{ $json.body.name }}. The webhook guide covers the setup and the difference between Test and Production URLs.

If the same person might submit twice, switch Append Row to Append or Update Row and set Column to match on to Email. A second submission from the same address updates the existing row instead of creating a duplicate.

Example 2: Send a morning list of items due today

This example uses a tab named Tasks with the headers Task, Owner, Due, and Status. Select the whole Due column and give it a custom date format of yyyy-mm-dd (for example 2026-10-27) from the Format menu. Get Row(s) reads the date as the text shown on screen and compares it exactly, so a consistent format is what makes the filter work. If you are unsure what the node will see, run Get Row(s) once without a filter and check the Due values in the output panel.

  1. Create a workflow with a Schedule Trigger. Set Trigger Interval to Days, Days Between Triggers to 1, Trigger at Hour to 8am, and Trigger at Minute to 0.
  2. Add a Google Sheets node with Operation set to Get Row(s) and Sheet set to Tasks. Under Filters, click Add Filter, choose the Due column, and set Value to {{ $today.toFormat('yyyy-MM-dd') }}. Then open the node's Settings tab and turn on Always Output Data.
  3. Add a Code node, keep Mode on Run Once for All Items with JavaScript, and paste the code below. It turns the rows into a single message.
  4. Finish with whatever notification node you use, such as Gmail or Telegram, and set the message body to {{ $json.text }}. Run it once manually, check the output, then click Publish.

To skip clicking through the first two nodes, copy this JSON and paste it onto the n8n canvas with Ctrl+V (Cmd+V on a Mac). Then swap in your spreadsheet URL and pick your credential:

{
  "nodes": [
    {
      "parameters": {
        "rule": { "interval": [ { "field": "days", "triggerAtHour": 8 } ] }
      },
      "name": "Every day at 8am",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [0, 0]
    },
    {
      "parameters": {
        "resource": "sheet",
        "operation": "read",
        "documentId": { "__rl": true, "mode": "url", "value": "https://docs.google.com/spreadsheets/d/YOUR_SPREADSHEET_ID/edit" },
        "sheetName": { "__rl": true, "mode": "name", "value": "Tasks" },
        "filtersUI": { "values": [ { "lookupColumn": "Due", "lookupValue": "={{ $today.toFormat('yyyy-MM-dd') }}" } ] },
        "options": {}
      },
      "name": "Rows due today",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [240, 0],
      "alwaysOutputData": true
    }
  ],
  "connections": {
    "Every day at 8am": { "main": [ [ { "node": "Rows due today", "type": "main", "index": 0 } ] ] }
  }
}

The Code node:

// Keep rows that have a task name and are not marked Done
const rows = $input.all()
  .map(item => item.json)
  .filter(r => r['Task'] && r['Status'] !== 'Done');

if (rows.length === 0) {
  return [{ json: { count: 0, text: 'Nothing is due today.' } }];
}

const lines = rows.map(r => `- ${r['Task']} (${r['Owner'] || 'unassigned'})`);

return [{
  json: {
    count: rows.length,
    text: `${rows.length} item(s) due today:\n${lines.join('\n')}`,
  },
}];

Always Output Data is the step people miss. When Get Row(s) finds nothing, it outputs nothing by default, so the rest of the workflow never runs and it looks broken. With the setting on, it emits one empty item, the filter in the Code node drops it, and you get "Nothing is due today." If you would rather hear nothing on quiet days, leave the setting off.

To include overdue items as well, remove the filter from Get Row(s), read the whole sheet, and filter in the Code node instead. Dates stored as yyyy-MM-dd text can be compared as plain strings, so replace the start of the code with:

const today = $today.toFormat('yyyy-MM-dd');

const rows = $input.all()
  .map(item => item.json)
  .filter(r => r['Task'] && r['Status'] !== 'Done' && r['Due'] <= today);

To mark reminded items afterwards, draw a second connection out of Get Row(s) into a Filter node that only passes rows whose Status is not Done, then into an Update Row node with Column to match on set to row_number, the value set to {{ $json.row_number }}, and Status set to Reminded. That Filter also removes the empty item produced by Always Output Data, which would otherwise make Update Row fail because row_number is empty.

Common errors and how to fix them

Error 400: redirect_uri_mismatch

If Google shows Error 400: redirect_uri_mismatch after you click Sign in with Google, the callback URL n8n sent does not match the one registered on your Google Cloud client. It must match character for character, including http versus https, domain, port, and any trailing slash. The usual causes are typing the URL by hand instead of copying it, changing your n8n URL (for example, moving to a custom domain) without updating Google, or self-hosting behind a reverse proxy without setting WEBHOOK_URL or N8N_EDITOR_BASE_URL, in which case n8n shows a callback URL starting with http://localhost:5678. Fix the first two by correcting the URL on the Google client (changes can take a few minutes to apply). For the third, set the correct public URL in n8n's environment variables, restart, and register the new callback URL. RoamerHost instances already have the public URL configured, so copying what n8n shows always works.

Error 403: access_denied

An access_denied page saying the app is being tested and only available to approved testers means the Google account you signed in with is not on the Test users list. Add it on the Audience page in Google Auth Platform and click Sign in with Google again. This often happens when the project was created with a work account but you authorize with a personal Gmail, or the other way around.

Everything breaks after a week: invalid_grant

If the Google Sheets node starts returning invalid_grant or "Token has been expired or revoked" about seven days after setup, it is almost always the Testing limit described above. Reopen the credential in n8n and click Sign in with Google to recover immediately, then publish the app or switch it to Internal so it does not happen again next week. Tokens can also stop working if you remove the app's access in your Google account security settings, or if the token goes unused for six months.

API has not been used in project, or it is disabled

An error saying Google Sheets API (or Google Drive API) has not been used in project 123456 before or it is disabled means the API is not enabled, or it was enabled in a different project. Check that the console is set to the project that owns your credential, enable both APIs, and try again after a minute or two.

Column names with spaces, or renamed headers

The node matches columns by header text, invisible spaces included. If a header is typed as "Email " with a trailing space, Map Automatically will not match your Email field to it and will add a new Email column at the right edge, leaving you with a duplicate. Remove the stray space in the sheet and keep headers short and free of spaces. To reference a field whose name contains a space in an expression, use {{ $json['Customer Name'] }}; dot notation fails.

If you rename headers after configuring the node, executions fail with "Column names were updated after the node's setup". Open the node and reselect the Mapping Column Mode, as the n8n docs recommend, so the node fetches the headers again, then fix the mappings. A Get Row(s) filter still pointing at an old name fails with The column "old name" could not be found; reselect the Column to fix it.

429: The service is receiving too many requests from you

You have hit Google's API quota. According to Google's documentation (updated September 2026), the Sheets API allows 300 read and 300 write requests per minute per project, and 60 of each per minute per user per project. There is no daily cap; once a minute's quota is used up you get a 429, and it resets the next minute. Keep in mind that one Append Row in n8n is more than one request, because it reads the header and existing data before writing. A webhook that receives dozens of submissions a minute, each triggering its own execution, can easily hit the 60-per-minute limit.

Append Row writes all items from a single execution in one batch, so batch wherever you can: collect incoming data in an n8n Data table or a database, and write it to the sheet every few minutes on a schedule. For workflows that write row by row in a loop, use Loop Over Items with a Wait node between batches. For occasional 429s, turn on Retry On Fail in the node's Settings and set Max. Tries and Wait Between Tries (ms). Google states that standard usage carries no extra cost, but also says that exceeding quota limits is planned to incur charges to your Google Cloud billing account later in 2026, so it is worth taming high-volume workflows now.

Wrong time zone: items are due today but nothing shows up

$today, $now, and the Schedule Trigger's 8am all follow the n8n time zone. A self-hosted instance without a setting defaults to America/New_York, so 8am in Taipei is still the previous evening in New York and $today is off by a day. Set the GENERIC_TIMEZONE environment variable to your zone (Asia/Taipei for Taiwan), or set Timezone in the workflow settings. RoamerHost instances use Asia/Taipei out of the box.

If you would rather not run the n8n server yourself

A reliable Google Sheets workflow needs n8n running around the clock, a stable HTTPS URL for OAuth callbacks and webhooks, and the right time zone. All of that is doable on your own, and the self-hosting cost breakdown shows what it takes. If you would rather skip server upkeep and version tracking, RoamerHost's fully managed n8n runs in an isolated container with its own HTTPS subdomain and webhook URLs, Taiwan time zone by default, automatic updates and automatic backups, and no caps on executions or workflows, for NT$5,988 billed yearly (avg. NT$499/mo), and Taiwan-based support when you get stuck.

FAQ

Q: Does connecting n8n to Google Sheets cost anything?

Not on the Google side for now. Standard Sheets API usage carries no extra cost as long as you stay within the per-minute quota, though Google plans to start charging for requests beyond quota later in 2026. On the n8n side it depends on how you run it: the self-hosted community edition is free but you maintain the server, n8n Cloud bills by executions, and managed hosting is a fixed yearly fee.

Q: Can one Google credential serve several workflows and spreadsheets?

Yes. An OAuth credential reaches every spreadsheet your account can open, so you create it once and select it in every Google Sheets node; a service account works with every file shared with it. Remember that quota is shared across the whole Google Cloud project, so the 300-per-minute project limit applies to all of those workflows combined.

Q: Can n8n trigger automatically when the sheet gets new data?

Yes, with the Google Sheets Trigger node. It polls the sheet on a schedule for added or updated rows rather than receiving pushes, and each check uses read quota. If you need an instant reaction, have the data source call an n8n webhook directly; it is faster and lighter on quota.

Q: Should data live in Google Sheets or an n8n Data table?

Use Google Sheets for data that people read, edit together, or analyze with pivot tables. Use n8n's built-in Data table for intermediate data only your workflows touch, such as a list of orders already processed; it is faster and not subject to Google API quotas. Many setups use both: workflows write to a Data table and copy a daily summary to a sheet for people to read.

Q: Can an AI Agent query Google Sheets directly?

Yes. The AI Agent node has a Google Sheets Tool that uses the same credential you created here, which is handy for letting an agent look up prices or stock levels. See the n8n AI Agent guide for the setup.

Sources: n8n Google Sheets node docs, n8n Google OAuth credential setup, n8n Google service account credential, n8n workflow publishing, Google Sheets API usage limits, Google OAuth 2.0 documentation, Google Auth Platform app audience.

Ready to get started with n8n?

60 seconds after you subscribe, n8n is installed for you — an isolated container with hard resource limits you never share, and HTTPS out of the box.

Billed yearly · no cancellation fee · cancel renewal anytime

Hi, I'm Roamer! Tap me anytime with a question and I'll help you out.

Roamer

Roamer - AI assistant

Online
Roamer

Ask me anything, anytime — I'll do my best to help!

Powered by RoamerHost AI