Build your first n8n workflow, and don't leave the door open
A webhook into a spreadsheet into a chat notification, built step by step and then actually secured. Most beginner guides stop at the editor, which is why most first workflows quietly do nothing in production and the ones that work are wide open.
By Parminder Kumar Sharma · · 12 min read

What you will have at the end
A workflow that receives data from outside, writes it to a spreadsheet, and posts a notification. It runs on a real URL rather than only inside the editor, which is the step most guides skip and the reason most first workflows quietly do nothing.
About forty minutes. No coding.
What you are building
- TriggerWebhook
- ActionAdd row to sheet
- ActionPost to channel

- 1The production URL Registered only while the workflow is Active
- 2Authentication Defaults to None on every new Webhook node
- 3Row appended The sheet node returns a confirmation, not your fields
- 4The message Reference the trigger to reach past the sheet node
Getting n8n running
n8n Cloud
- Nothing to install and nothing to keep patched.
- Webhook URLs are publicly reachable immediately, which matters more than it sounds.
- Fastest route to a working first workflow.
Docker, self-hosted
- Your data stays on infrastructure you control.
- Pins a known-good runtime; upgrades are a new image tag.
- You are now responsible for exposing it safely, which is a real job.
$ docker volume create n8n_data && docker run -it --rm --name n8n -p 5678:5678 -v n8n_data:/home/node/.n8n docker.n8n.io/n8nio/n8nEditor is now accessible via:
http://localhost:5678The named volume is not optional. Without it every credential and workflow you create disappears when the container stops.
Open http://localhost:5678. You will get a welcome screen or the Overview page. Choose Start from Scratch, or Create Workflow.
Building it
Add the trigger
Click Add first step. Search for Webhook and select the Webhook node. Set HTTP Method to POST.
The two URLs are the important part of this screen, and a later section is entirely about why.
You should see: A Webhook node on the canvas with its panel open, showing two URLs labelled Test and Production.
Send it some data
Click Listen for Test Event. The node is now waiting. From a terminal, post something to the Test URL.
You should see: A confirmation in the editor, and the JSON you sent appearing in the OUTPUT panel.
$ curl -X POST 'http://localhost:5678/webhook-test/YOUR-PATH' -H 'Content-Type: application/json' -d '{"name":"Test Person","email":"someone@example.com","topic":"ISO 27001"}'{"message":"Workflow got started."}Replace YOUR-PATH with the path shown in the node. example.com is the reserved documentation domain and is safe in examples.
Write it to a spreadsheet
Click the Add node connector on the Webhook node. Search for your spreadsheet tool and choose the action that appends a row.
A Credential for ... API dropdown appears. Choose Create new credential and complete sign-in. This is one-off and reused by every node that needs it.
Map the columns with expressions rather than typed values: {{ $json.name }}, {{ $json.email }}, {{ $json.topic }}. Click Execute step.
You should see: A new row containing your values, not the literal text {{ $json.name }}.
Post the notification
Add a third node for your chat tool, choose the send-message action, create its credential, and pick a channel. For the text: New enquiry from {{ $json.name }} about {{ $json.topic }}.
You should see: A message in your channel. If it contains field names rather than values, read the warning below.
Watch what each node actually receives
- TriggerWebhook
{ "name": "Test Person", "email": "someone@example.com", "topic": "ISO 27001" } - ActionAdd row to sheet
{ "spreadsheetId": "1a2b3c", "updatedRange": "Sheet1!A2:C2", "updatedRows": 1 } - ActionPost to channel
{ "ok": true, "channel": "C01ABCDEF", "ts": "1754300000.000100" }
Each node receives the output of the one before it, not the original trigger data. That single fact explains most of the confusion in a first workflow: if a field has vanished by step three, it is because step two did not pass it on.
The step that decides whether any of this was real
Your workflow runs in the editor. It is not yet running in the world.
Two URLs, two behaviours
- Test URL/webhook-test/...
- Production URL/webhook/...
The Test URL listens only while you are watching, after you click Listen for Test Event. The Production URL is registered only when the workflow is Active, and it never shows anything in the editor: its runs appear in the Executions tab.
Save the workflow, turn on Active, send the same request to the production path (/webhook/ rather than /webhook-test/), and check Executions.
When it does not work, which it will not the first time
Four failures account for nearly all of them, and each looks like something else, which is why they cost an evening.
What you see, and what it actually is
| What you see | What it usually is | How to confirm it |
|---|---|---|
| The webhook URL returns 404 | You are on the test URL and the workflow is not listening | Test URLs are live only while the editor is waiting. Activate the workflow and use the production URL |
| It worked once, then stopped | Same cause. The test listener fired once and closed | Check whether the workflow is toggled active, not whether it is saved |
| The node runs but receives nothing | The payload arrived in a shape the expression does not match | Open the execution and read the actual input rather than the expression |
| A credential fails only in production | Two credentials with the same name, or an environment variable not present in the running container | Open the credential from the node itself rather than from the list |
The first two are the same mistake and between them they account for most first-workflow frustration. A test URL exists to let you watch data arrive while you build. It is not the address anything should ever call in earnest, and n8n does not warn you when you use it that way.
Why the webhook is the interesting part
It is worth pausing on what you just built, because the same shape underlies most of the things that go wrong with automation platforms later.
A webhook is a public endpoint. Not public in the sense of documented, but public in the sense that anybody who learns the URL can call it, and URLs leak through browser history, screenshots, logs and pasted messages. There is no authentication on it unless you add one, and n8n will happily activate it without asking whether you meant to.
Whatever arrives at that endpoint becomes the input to every node downstream. If a later node uses that input to build a query, a file path or a request to another system, then a stranger is supplying part of it. That is the same property as prompt injection, one layer down: the system has no way to tell data you expected from data somebody else chose to send.
So two habits, from the first workflow rather than the tenth. Validate the shape of what arrives before anything acts on it, and reject what does not match rather than letting a malformed payload flow onward. And put authentication on the endpoint, even for something internal, because internal is a description of intent rather than of who can reach a URL.
Now secure it, because right now it is not
Here is the part the beginner guides leave out. The webhook's Authentication setting defaults to None. The URL you just created is an unauthenticated public endpoint that writes to your spreadsheet and posts into your team's chat, for anyone who learns the address.

Addresses leak in ordinary ways: a browser extension, a pasted debugging snippet, a screenshot in a ticket. Nobody has to attack you for this to go wrong.
Set one of these
- Header auth
- Basic auth
- JWT auth
- IP whitelisting, alongside one of the above
What each is for
- A shared secret in a header. The usual answer for service-to-service calls.
- Fine over HTTPS, and only over HTTPS.
- When the caller already issues signed tokens.
- Narrows who can reach it. Not sufficient alone: addresses can be spoofed and offices share them.
Three more things before this carries anything real:
- Treat the payload as untrusted. Anyone who can reach the URL controls every field in it.
- Never paste a secret into a node field. Use a credential, which n8n stores encrypted, rather than a parameter that lives in the workflow JSON and travels with every export.
- Put the instance behind HTTPS if anything but your laptop can reach it. Basic auth over plain HTTP hands the credential to the network.
What to change before this is anything but a demo
The workflow now runs, which is a different thing from being ready to leave running. Four changes, in this order, and none takes long.
Give it its own credential. The one you created while building probably has more access than the workflow needs, because you granted whatever made the first test pass. Create a second with the narrowest scope that still works, and confirm the workflow fails with the old one removed.
Decide what happens on failure. A workflow with no error branch fails silently, which means the first you hear of it is somebody asking why the rows stopped appearing. An error trigger sending one message somewhere a person looks is the whole fix.
Turn off execution data retention, or shorten it. Every run stores its full input and output. If this workflow touches personal data, that is a second copy of it sitting in a debugging log, and the retention period is a decision rather than a default.
Write down what it does and who owns it. One paragraph, wherever your runbooks live. The workflow you understand completely today is the one somebody else will be looking at in nine months without you.
What good looks like
Take this with you
Before a workflow carries anything that matters
- Authentication is set to something other than None, and the secret lives in a credential rather than a node field.
- The workflow is Active and the caller uses the production URL.
- Credentials are scoped to the one sheet and the one channel this workflow needs, not the whole account.
- There is an error path, so a failure alerts someone instead of vanishing into the Executions log.
- Every field arriving from outside is treated as untrusted, especially before it reaches a model.
- The instance is behind HTTPS if anything other than your own machine can reach it.
Verified
Steps and interface labels checked against n8n's own documentation on 4 August 2026. Labels move between releases; the data-flow behaviour described here has been stable far longer and is the part worth learning.


