Time required. 30-50 minutes.
Prerequisites. A Google Workspace account with admin access, A running n8n instance (self-hosted or cloud), A Google account that can create GCP projects, A shared Google Calendar for the PTO events.
Install the PTO Request Form workflow. From employee submission to manager approval through a shared team calendar event, every step runs on n8n.
The workflow lets employees request paid time off through a single Google Form. The manager receives an approval email, and on approval the workflow adds the dates to a shared team calendar, updates the Sheet row to Approved or Rejected, and emails the employee with the outcome.
It uses Gmail, Google Sheets, Google Calendar, and a Google Service Account with Domain-Wide Delegation. Three credentials in n8n.
Employee submits the form: The Google Form submission triggers an Apps Script on the linked Sheet. The script reads the new row by header name and posts the form data (signed with HMAC) to the n8n form webhook.
n8n logs the request and notifies the manager: The workflow creates a new row in the Sheet with Status = Pending and sends the manager an HTML email with two buttons: Approve and Reject. The decision URL is derived from the form webhook URL by replacing /pto-request-form with /pto-request-decision and appending eight query parameters: action, email, name, start, end, row, type, and notes. Each value is percent-encoded (URL-encoded) so free-text fields like notes are safe.
Manager clicks Approve or Reject: The decision webhook is a GET request (no body, no HMAC). It validates the action, then either:
Approved with a decision timestamp, and emails the employee with the outcome.Rejected with a decision timestamp and emails the employee.Both paths (form submission and decision) are handled by a single workflow using two webhook triggers.
The Google Form must have these seven fields, in any order. The Apps Script matches them by header label, not position, so you can reorder the form fields freely:
| Form field | Sheet column header | Required? |
|---|---|---|
| Employee name | Employee name | Yes |
| Employee email | Employee email | Yes |
| Manager email | Manager email | Yes |
| PTO type | PTO type | Yes (dropdown: Vacation, Sick, Personal) |
| Start date | Start date | Yes |
| End date | End date | Yes |
| Notes | Notes | No (optional) |
The workflow writes these three columns to the sheet automatically. The Apps Script setup function adds them to the sheet and the workflow sets values on the row:
| Column | Set by | When |
|---|---|---|
| Status | n8n | Pending on form submit, Approved/Rejected on decision |
| Decision timestamp | n8n | On decision (ISO 8601) |
| Calendar event ID | n8n | On approval (the event ID returned by the Calendar API) |
In addition, the Apps Script setup function creates an Error Log tab with these columns:
| Column | Type | Description |
|---|---|---|
| timestamp | date | ISO 8601, logged when an error occurs |
| error_type | string | validation_error, duplicate_error, or step_failure |
| employee_email | string | From the triggering record |
| employee_name | string | From the triggering record |
| node_name | string | Which node failed |
| error_message | string | Human-readable cause |
| resolved | string | Always "No" on insert; edit to "Yes" when resolved |
The Error Log tab stays empty during normal operation. It only gets populated when something fails (missing fields, API errors, etc.).
Form Responses 1 tab with the 8 form columns. Do not add any extra columns to this tab yourself: the setup() function will add the n8n-managed columns after linking.https://your-n8n.com/rest/oauth2-credential/callbackNote: You will only see the client secret once, so make sure to copy and/or download the JSON.
The workflow needs a shared Google Calendar to check for conflicts and add approved PTO events.
You can get the Calendar ID in two ways. The configurator accepts either format:
Option 1: Copy from Integrate calendar (recommended)
c_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b@group.calendar.google.comOption 2: Paste the share link
Important: The shared calendar must be owned by (or shared with) the Google Workspace admin account that the Service Account impersonates, with Make changes to events permission. This allows the Service Account to create events on the calendar via domain-wide delegation. No separate Google Calendar OAuth2 credential is needed; the workflow uses the HTTP Request node with the Service Account to call the Calendar API directly.
To share: go to the calendar's Settings and sharing, under Share with specific people, add the Service Account impersonated user's email with Make changes to events permission.
You'll need this Calendar ID when configuring the workflow.
The Google Calendar API must be enabled in your Google Cloud project before the Service Account can use it. The Calendar API is not on by default, and the PTO workflow's Create Calendar Event node fails without it.
If you skip this step, the Create Calendar Event node will fail with:
Google Calendar API has not been used in project [PROJECT_ID] before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/calendar-json.googleapis.com/overview?project=[PROJECT_ID] then retry.
n8n-service-account@project-name.iam.gserviceaccount.com)Warning: New Google Workspace organizations have the following org policies enforced by default which will block service account and key creation:
iam.managed.disableServiceAccountCreationiam.disableServiceAccountKeyCreationiam.managed.disableServiceAccountKeyCreationBefore creating your service account key, you must temporarily unenforce these policies:
- Go to console.cloud.google.com
- In the top search bar, search for Organization Policies
- Note: The console only shows 50 policies at a time by default. Scroll past the first 50 or search by name to find each policy
- For each of the three policies listed above, click on it, select Override parent's policy, set to Not enforced, and save
Go to admin.google.com
Go to Security → Access and data control → API controls
Scroll down and click Manage Domain Wide Delegation
Find your Service Account's existing entry in the list and click Edit. If no entry exists yet, click Add new instead.
Paste in the Service Account Client ID (if adding new) or confirm it matches (if editing)
In the OAuth scopes field, add the following scopes (if editing, append to any existing scopes):
https://www.googleapis.com/auth/calendar.events,https://www.googleapis.com/auth/spreadsheets.readonly
These are the only two scopes the Service Account needs for this workflow. The Calendar Events API is used to create the PTO event on the shared calendar (POST /calendar/v3/calendars/{CALENDAR_ID}/events), and the Google Sheets API is used in read-only mode to fetch the existing row status before acting on Approve/Reject clicks (GET /v4/spreadsheets/{SHEET_ID}/values/Form Responses 1!A1:Z{row}). The native n8n Gmail and Google Sheets credentials used elsewhere in the workflow carry their own OAuth consent scopes and do not need to be added here.
Using multiple templates with the same Service Account? The combined scopes across all Domain-Wide Delegation-using templates are
admin.directory.user(for onboarding and offboarding),calendar.events(for PTO), andspreadsheets.readonly(for PTO). Comma-separated scopes go in one OAuth scopes field. Addingcalendar.events, spreadsheets.readonlyto an existing onboarding or offboarding Domain-Wide Delegation entry is all you need to also run PTO. Do not create a duplicate entry.
Click Authorize
Note: Domain Wide Delegation can take up to 24 hours to propagate but in my experience it updates fairly immediately.
Once your service account credential is confirmed working in n8n, re-enforce the three org policies to secure your Google Workspace:
Go to console.cloud.google.com
In the top search bar, search for Organization Policies
For each of the three policies below, click on it, select Override parent's policy, set to Enforced, and save:
iam.managed.disableServiceAccountCreationiam.disableServiceAccountKeyCreationiam.managed.disableServiceAccountKeyCreationThis ensures your organization is protected against unauthorized service account and key creation going forward.
Verify before moving on. All three policies should now read Enforced. If any still reads Not enforced, your Workspace org can still create service accounts and JSON keys without restriction. The workflow above this section will keep running either way, so this step won't fail loudly if it slips past you.
When you create this credential in n8n, it is labeled Google Service Account (with DWD) by default. Keep that name during initial setup so the imported workflow auto-links the credential; after the link is established, n8n resolves it by credential ID at runtime, so any later rename is harmless.
client_email) and paste it into the Service Account Email fieldprivate_key) and paste the full value into the private key field (everything between and including -----BEGIN PRIVATE KEY----- and -----END PRIVATE KEY-----\n)https://www.googleapis.com/auth/calendar.eventssheets.googleapis.com,www.googleapis.comThe PTO workflow's service-account-impersonated calls reach only two Google APIs: the Google Calendar API at
www.googleapis.com(theCreate Calendar Eventnode POSTs tohttps://www.googleapis.com/calendar/v3/calendars/{CALENDAR_ID}/events), and the Google Sheets API in read-only mode atsheets.googleapis.com(theRead Decision Statusnode GETshttps://sheets.googleapis.com/v4/spreadsheets/{SHEET_ID}/values/Form Responses 1!A1:Z{row_number}to look up the existing row byrow_numberbefore applying a decision). Ifsheets.googleapis.comis missing from this allow-list, that lookup fails with:Domain not allowed: This credential is restricted from accessing https://sheets.googleapis.com/v4/... Only the following domains are allowed: www.googleapis.com.
The Service Account credential in n8n must have the following four settings configured correctly, otherwise the Create Calendar Event node will fail with Forbidden - perhaps check your credentials? / Request had insufficient authentication scopes. The same credential is used by Read Decision Status to call the Google Sheets API; if its allow-list excludes sheets.googleapis.com, the lookup returns a domain error and the workflow falls back to treating the row as Pending (the approve/reject still lands but idempotency is lost for clicks that hit a row already updated).
To check or edit these on an existing credential:
-----BEGIN PRIVATE KEY-----)https://www.googleapis.com/auth/calendar.eventssheets.googleapis.com,www.googleapis.comIf any of these four settings is wrong:
calendar.events → Google rejects the Calendar API call with insufficient scopeswww.googleapis.com → HTTP Request nodes are blocked from calling the Calendar API even with a valid scopesheets.googleapis.com → Read Decision Status returns a domain-error fallback that pretends the row is still pending, so the approve/reject still lands but idempotency is lost on second clicksunauthorized_client
n8n validates all Domain-Wide Delegation scopes when authenticating, not just the ones your workflow uses. If any scope is missing the entire authentication fails. Make sure the full scope list is added exactly as shown in the Google Workspace Admin Console under Security → Access and data control & API controls → Manage Domain Wide Delegation. No extra spaces, no missing scopes.
invalid_grant (Private key validation failed)
This is usually a propagation delay. After setting up or updating Domain-Wide Delegation in the Admin Console, it can take up to a few hours to take effect. If you see this error immediately after setup, wait a few hours and try again before troubleshooting further.
HTTP Request Domain blocked
The "Set up for use in HTTP Request node" toggle on the Google Service Account credential in n8n must be turned on, and the Allowed HTTP Request Domains must be set to Specific Domains with sheets.googleapis.com,www.googleapis.com. If the Calendar API calls fail, double-check that www.googleapis.com is included.
Forbidden - Request had insufficient authentication scopes
The Calendar API rejected the request because the OAuth token does not include the calendar scope. Two things must both be true:
https://www.googleapis.com/auth/calendar.events in its Scope field (per the Required n8n credential settings section above)If both are set, propagation may be the cause; Domain-Wide Delegation changes can take up to a few hours to take effect.
Each credential you create in n8n has a unique ID. You need these IDs to configure your workflow.
You need three credential IDs (a separate Calendar credential is not required, since the Service Account handles Calendar API access via HTTP Request):
The PTO Request Form workflow uses a two-step approval process:
Employee submits form: The Google Form submission triggers the Apps Script, which sends the data to the n8n webhook. The workflow creates a record in the Google Sheet with status Pending and sends an email to the manager.
Manager approves or rejects: The email sent to the manager contains two links. Both point to the same webhook URL at the pto-request-decision path and append eight query parameters (action, email, name, start, end, row, type, notes). All values are percent-encoded (URL-encoded) so free-text fields like the notes parameter are safe when they contain spaces or other reserved characters.
https://your-n8n.com/webhook/pto-request-decision?email=…&name=…&start=YYYY-MM-DD&end=YYYY-MM-DD&row=N&type=Vacation¬es=…&action=approvehttps://your-n8n.com/webhook/pto-request-decision?email=…&name=…&start=YYYY-MM-DD&end=YYYY-MM-DD&row=N&type=Vacation¬es=…&action=rejectThe action parameter (approve or reject) is what tells the workflow which decision was made. The other seven parameters are used by the workflow to identify the row (row), confirm the request dates (start and end), and enrich the manager-email context (name, email, type, notes).
Workflow processes the decision .
Both paths (form submission and decision) are handled by a single workflow using two webhook triggers. The forms webhook (pto-request-form) receives submissions from the Apps Script, and the decision webhook (pto-request-decision) receives approval or rejection clicks.
Want to skip the manual setup?
Our templates include a configuration tool. Enter your details once and download a ready-to-import workflow. View templates →
https://tl-hr.com/configure/pto-request-form-google-workspace| Field | Description |
|---|---|
| Company name | Your company name |
| Google Workspace domain | e.g. acme.com |
| Gmail credential ID | The ID from the Gmail credential in n8n |
| Google Sheets credential ID | The ID from the Sheets credential in n8n |
| Google Service Account credential ID | The ID from the Service Account credential in n8n |
| n8n webhook URL (base) | e.g. https://your-n8n-instance.com/webhook/pto-request-form. That is the form webhook path |
| Shared Calendar ID | The Calendar ID from your shared PTO calendar (looks like c_abcdef123456@group.calendar.google.com) |
| Google Sheets ID | The ID from your Google Sheet URL (the long string in https://docs.google.com/spreadsheets/d/THIS-ID/edit) |
| Alert email | Email to receive error messages if the workflow fails |
Important: The Apps Script is installed on the Google Sheet (not the Form). Unlike other templates where the script goes on the Form, the PTO script uses a Sheet-based trigger that fires when a new form response row is added. This allows it to pass the row number to the workflow so the workflow can update the correct row when the manager approves or rejects.
Code.gs file should be emptypto-request-form-google-workspace-appscript.gs file in a text editorCode.gs filesetup in the function dropdown and click the Run buttonpto-request-form-google-workspace-configured.json fileReady to automate?
The full PTO Request Form workflow includes the complete workflow, Apps Script, error handling, automatic retry, and the configuration tool. Setup takes 30-60 minutes if you follow this guide.
action=approve in the URLaction=reject in the URL