Introduction
In this tutorial you’ll build your first End-to-End n8n Workflow that captures a webhook, validates incoming JSON, and saves cleaned records to Google Sheets. “First End-to-End n8n Workflow” is the exact skill you need to move from tinkering to production-ready automations. In my projects, starting with a simple webhook → validate → persist pattern has saved countless hours of debugging and reduced noisy data entering my systems.
This guide covers prerequisites, step-by-step instructions, node settings, a reusable validation Function snippet, screenshots placeholders, best practices, common pitfalls and fixes, and a short FAQ. It also references n8n HTTP Request node alternatives, n8n error handling patterns, and how you can run this in a self-hosted n8n workflow.
Prerequisites
- An n8n instance (cloud or self-hosted). I usually test locally and deploy to my self-hosted n8n for production.
- A Google account and a Google Cloud project with Google Sheets API enabled.
- Google Sheets credentials in n8n (OAuth2 client or Service Account — I prefer service accounts for servers).
- Basic familiarity with n8n canvas, nodes, and expressions.
If you need credential setup help, see our guide on [n8n Fundamentals](/fundamentals) and the Google Cloud docs to enable the Sheets API.
Why this workflow? (quick conceptual overview)
A reliable End-to-End n8n Workflow follows this pattern:
1. Ingest (Webhook node)
2. Validate & Normalize (Function or Code node)
3. Conditional routing (IF node)
4. Persist (Google Sheets node)
5. Optional: Notifications or retries (HTTP Request, Email, or Slack nodes)
This structure keeps integrations decoupled and makes error handling predictable. I use a similar pattern across CRM automation and email automation projects.
Step-by-step guide
1. Configure the Webhook node
– Add a Webhook node to your canvas.
– Set HTTP Method to POST.
– Choose Response Mode: “On Received” (or “Last Node” if you want to craft a custom response later).
– Note the Webhook URL and test payload format.
Example test payload (JSON):
json
{
"name": "Alice Example",
"email": "alice@example.com",
"source": "signup_form",
"created_at": "2026-02-10T12:00:00Z"
}

2. Add a Function node for validation & normalization
– Add a Function node after the Webhook node.
– Purpose: validate required fields, normalize timestamps and ensure data types.
– In my projects, I keep validation logic small and explicit — avoid copying large validation libraries into Function nodes.
Paste this JavaScript into the Function node:
javascript
// Simple validation / normalization in n8n Function node
const input = items[0].json;
const errors = [];
// Required fields
if (!input.name || typeof input.name !== 'string') {
errors.push('name is required and must be a string');
}
if (!input.email || !/^\S+@\S+\.\S+$/.test(input.email)) {
errors.push('email is required and must be valid');
}
// Optional: normalize created_at to ISO
let createdAt = input.created_at;
if (createdAt) {
const ts = Date.parse(createdAt);
if (isNaN(ts)) {
errors.push('created_at must be a valid date');
} else {
createdAt = new Date(ts).toISOString();
}
} else {
createdAt = new Date().toISOString();
}
if (errors.length) {
return [{ json: { valid: false, errors } }];
}
// If valid, return normalized payload
return [{ json: { valid: true, payload: { name: input.name.trim(), email: input.email.toLowerCase(), source: input.source || 'unknown', created_at: createdAt } } }];
– The Function node returns a small envelope: { valid: boolean, payload: {…} } so downstream nodes can branch safely.
3. Add an IF node to route valid vs invalid data
– Add an IF node connected to the Function node.
– Configure the condition: Expression mode with value
– Left value: {{$json[“valid”]}}
– Condition: is equal to
– Right value: true
– The TRUE branch handles valid payloads; FALSE branch logs errors or notifies a team.
4. Prepare the data for Google Sheets
– On the TRUE branch, add a Set node to map fields to columns. Example keys: name, email, source, created_at.
– Set node fields (use exactly these keys):
– name = {{$json[“payload”][“name”]}}
– email = {{$json[“payload”][“email”]}}
– source = {{$json[“payload”][“source”]}}
– created_at = {{$json[“payload”][“created_at”]}}
– This explicit mapping means your Google Sheets node receives predictable keys.
5. Configure the Google Sheets node to append rows
– Add a Google Sheets node after the Set node.
– Select credentials (Service Account or OAuth2 credential you created).
– Resource: “Sheet” (or “Spreadsheet” depending on your n8n version), Operation: “Append”.
– Spreadsheet ID: paste your spreadsheet ID (from the URL).
– Range: Sheet1!A:D (or the sheet and range that match your columns).
– Value Input Mode: USER_ENTERED (to allow dates and formulas to be parsed).
– Map the incoming JSON fields to columns: if you used Set with keys name,email,source,created_at the node will append in that order to A-D.
Note: If your n8n version shows a mapping UI, map the fields explicitly. If it expects values array, convert the Set output using a small Function node to return an array like [[name, email, source, created_at]].

6. Handle invalid payloads (FALSE branch)
– On the IF node FALSE branch, add a node to capture the incoming errors. Options:
– Save the invalid payload & errors to a Slack channel (Slack node)
– Send an email with the error details (Email node)
– Append the error to a separate Google Sheet or log table
– Example: Add a Google Sheets node to append errors to a “failed_rows” sheet with columns: payload, errors, received_at.
7. Test the workflow
– Activate the webhook or execute it via an HTTP client (curl, Postman).
– Watch the executions in n8n. I always run a few malformed payload tests to ensure the IF branching works as expected.
Example curl:
bash
curl -X POST 'https://your-n8n-instance/webhook/my-webhook-path' \
-H 'Content-Type: application/json' \
-d '{"name":"Test","email":"test@example.com"}'
Screenshots placeholders
Best practices
Common pitfalls & fixes
FAQ
How can I secure my webhook publicly?
Use authentication (API keys) in the Webhook node and validate the key inside the Function node. Alternatively, front the webhook with an API gateway that handles authentication. For self-hosted n8n workflow instances, enable HTTPS and restrict inbound IPs when possible.
Can I use the n8n HTTP Request node instead of a Webhook?
Yes — the n8n HTTP Request node is useful for polling or calling external APIs. A Webhook is push-based and ideal for instant ingestion. I use HTTP Request nodes in downstream enrichment steps (e.g., lookup by email) to reduce incoming webhook latency.
What about schema validation libraries (AJV) inside n8n?
AJV isn’t available by default in a vanilla n8n Function node. You can implement simple validations in JavaScript or run a validation microservice and call it via HTTP from the workflow. For heavy validation, keep it out of the workflow to preserve performance.
How do I monitor this workflow in production?
Use the n8n executions UI to inspect failures, enable workflow-level retried executions, and append logs to a central monitoring sheet or service. I also use alerts (Slack or email) for repeated failures.
Conclusion
You now have a working First End-to-End n8n Workflow: a webhook listener that validates incoming payloads and safely appends clean rows to Google Sheets. In my experience, this pattern is the foundation for many integrations — from CRM automation to lead enrichment and form processing.
Next steps:
Try this in your n8n instance today and iterate — a well-tested webhook → validation → persist pipeline will pay dividends as you scale automation across teams.