Build Your First n8n Workflow: Webhooks, API Calls, and Notifications
Build two real n8n workflows from scratch on your self-hosted instance. A webhook that fetches weather data and sends it to Discord, and an RSS monitor that posts new articles to a channel.
You have n8n running on your VPS. Now what?
In this tutorial, you build two real workflows from scratch. Not toy examples that echo data back to nowhere. These are automations you will actually keep running:
- Weather webhook: receive an HTTP request, call the wttr.in weather API, format the response, and send it to a Discord channel.
- RSS monitor: check a blog's RSS feed on a schedule and post new articles to Discord.
Both workflows cover core n8n concepts as you go: nodes, connections, expressions, the draft/publish system from n8n 2.0, and error handling.
Prerequisites:
- A running n8n instance on your VPS. If you do not have one yet, follow Install n8n with Docker Compose on a VPS first.
- A Discord server where you can create a webhook (or a Slack workspace. Instructions are interchangeable; we use Discord here).
- Basic comfort with the terminal and
curl.
What are n8n nodes and connections?
Nodes are individual steps in a workflow. Each node does one thing: receive a trigger, make an HTTP request, transform data, or send a message. Connections are the lines between nodes. They pass data from one node's output to the next node's input. Every workflow is a chain of nodes connected left to right.
The node types used in this tutorial:
| Node | Purpose |
|---|---|
| Webhook | Listens for incoming HTTP requests and starts the workflow |
| HTTP Request | Calls an external API and returns the response |
| Set | Creates or reshapes data fields |
| If | Branches the workflow based on a condition |
| Discord | Sends a message to a Discord channel via webhook |
| RSS Feed Trigger | Polls an RSS feed and fires when new items appear |
| Error Trigger | Catches workflow failures and runs a notification workflow |
How do you build your first n8n workflow from scratch?
Open your n8n instance in the browser. Click Add workflow on the Workflows page. n8n drops you on an empty canvas with a trigger node placeholder.
This first workflow does the following: receive a city name via an HTTP webhook, fetch the current weather from wttr.in, format the result, and post it to a Discord channel.
Step 1: Create a Discord webhook
Before building in n8n, you need a place to send notifications.
- Open your Discord server. Right-click the channel where you want weather updates.
- Select Edit Channel > Integrations > Webhooks.
- Click New Webhook. Give it a name like
n8n Weather Bot. - Click Copy Webhook URL. Save this URL. It looks like:
https://discord.com/api/webhooks/123456789/abcDEF...
Treat this URL like a password. Anyone who has it can post to your channel.
Step 2: Add the Webhook trigger node
Click the + button on the canvas trigger placeholder. Search for Webhook and select it.
Configure the webhook:
- HTTP Method: POST
- Path:
weather
Leave everything else at the defaults. The path is what makes your webhook URL unique. With the path set to weather, your webhook URLs will be:
- Test URL:
https://your-n8n-domain.com/webhook-test/weather - Production URL:
https://your-n8n-domain.com/webhook/weather
Notice the difference: /webhook-test/ vs /webhook/. This distinction matters. More on that in a moment.
Step 3: Test the webhook with curl
Click Listen for test event in the webhook node. n8n now waits for an incoming request on the test URL.
Open a terminal on your local machine and send a test request:
curl -X POST https://your-n8n-domain.com/webhook-test/weather \
-H "Content-Type: application/json" \
-d '{"city": "Paris"}'
Switch back to n8n. The webhook node should show a green checkmark with the received data. You will see the JSON body with city: "Paris" in the output panel.
If nothing happens, check that:
- Your n8n instance is accessible from the internet (reverse proxy is configured).
- You clicked Listen for test event before sending the curl request.
- The URL matches exactly, including the
/webhook-test/prefix.
Step 4: Add the HTTP Request node (call the weather API)
Click the + button on the right side of the Webhook node. Search for HTTP Request and add it.
Configure it:
- Method: GET
- URL:
https://wttr.in/{{ $json.city }}?format=j1
That {{ $json.city }} part is an n8n expression. It pulls the city field from the incoming webhook data. When someone sends {"city": "Paris"}, n8n replaces the expression with Paris, making the final URL https://wttr.in/Paris?format=j1.
The ?format=j1 parameter tells wttr.in to return structured JSON instead of the ASCII art weather report.
Click Test step to run just this node. You should see a JSON response with weather data including fields like current_condition, temp_C, weatherDesc, and more.
Sharp eyes moment: look at the output panel. The response is nested. The current temperature lives at $json.current_condition[0].temp_C and the description at $json.current_condition[0].weatherDesc[0].value. You will need these paths in the next step.
Step 5: Add a Set node to format the message
The raw weather API response is large. You only need a few fields for the Discord message.
Add a Set node after the HTTP Request node. Click Add field and create these assignments:
| Field name | Type | Value (expression) |
|---|---|---|
message |
String | Weather in {{ $('Webhook').item.json.city }}: {{ $json.current_condition[0].temp_C }}°C, {{ $json.current_condition[0].weatherDesc[0].value }}. Humidity: {{ $json.current_condition[0].humidity }}%. Wind: {{ $json.current_condition[0].windspeedKmph }} km/h. |
The {{ $('Webhook').item.json.city }} expression reaches back to the Webhook node's output to get the city name. The $json references without a node name pull from the immediately preceding node (HTTP Request).
Click Test step. The output should show a single message field with something like:
Weather in Paris: 14°C, Partly cloudy. Humidity: 72%. Wind: 15 km/h.
Step 6: Add the Discord node
Add a Discord node after the Set node.
- Set Connection Type to Webhook.
- Under Credential for Discord, click Create New Credential.
- Paste your Discord webhook URL from Step 1 into the Webhook URL field. Click Save.
- Set Operation to Send a Message.
- In the Message field, use the expression:
{{ $json.message }}
Click Test step. Check your Discord channel. You should see the weather message posted by your bot.
Verification: Open Discord. The message should appear in the channel you configured the webhook for. If it does not, double-check the webhook URL in the credential settings.
Step 7: Test the full workflow
Go back to the Webhook node. Click Listen for test event again. Send another curl request:
curl -X POST https://your-n8n-domain.com/webhook-test/weather \
-H "Content-Type: application/json" \
-d '{"city": "Tokyo"}'
Watch the execution flow through all four nodes. Each node turns green as it completes. A weather report for Tokyo should appear in your Discord channel.
Check the Executions tab on the left sidebar. You will see a log entry for this test execution with timing, status, and the data at each node. This execution log is where you debug problems.
How does the n8n webhook node receive external data?
The Webhook node creates an HTTP endpoint on your n8n instance. When an external service (or a curl command) sends a request to that endpoint, n8n receives the data and starts the workflow. The node supports GET, POST, PUT, PATCH, DELETE, and HEAD methods. POST with a JSON body is the most common pattern.
How do you test an n8n webhook with curl?
You already did this above. Here is what matters:
| Test URL | Production URL | |
|---|---|---|
| Path format | /webhook-test/<path> |
/webhook/<path> |
| When active | Only while you click "Listen for test event" or "Execute workflow" in the editor | Only after you publish the workflow |
| Shows data in editor | Yes, live in the node output panel | No (check Executions tab) |
| Use case | Building and debugging | Real external integrations |
During development, always use the test URL. It lets you see data flowing through each node in real time. The production URL only works after you publish the workflow.
What is the difference between draft and published workflows in n8n 2.0?
n8n 2.0 separated saving from activating. Your edits stay in draft and do not affect the live version until you explicitly publish. Since n8n 2.4 (January 2026), the editor autosaves your work every few seconds. There is no manual Save button anymore.
Here is the sequence:
- You build and test your workflow using the test URL. n8n autosaves as you go.
- Your changes exist only in draft. The live version (if any) keeps running unchanged.
- You click Publish. Now the production URL is active and the workflow runs automatically when triggered.
- If you edit the workflow later, your changes stay in draft until you publish again.
In n8n 1.x, saving and activating were the same action. They are not anymore.
Publish your weather workflow now. Click the Publish button in the top right (or press Shift+P). The workflow status changes to "Active."
Verify with curl using the production URL this time:
curl -X POST https://your-n8n-domain.com/webhook/weather \
-H "Content-Type: application/json" \
-d '{"city": "Berlin"}'
Check Discord. The Berlin weather report should appear. Then check the Executions tab in n8n to see the production execution logged.
How do you handle errors in an n8n workflow?
The Error Trigger node catches workflow failures and runs a separate notification workflow. When an active workflow fails (for example, the weather API is down or Discord rejects the message), n8n fires the Error Trigger in the linked error workflow. This only works for published, automatically-triggered executions. Manual test runs do not trigger it.
Build an error notification workflow
- Go back to the Workflows page. Click Add workflow. Name it
Error Handler. - Add an Error Trigger node as the trigger. This node receives error data automatically when a linked workflow fails.
- Add a Discord node after the Error Trigger.
- Configure it with the same Discord webhook credential you created earlier.
- Set the message to:
⚠️ Workflow failed: {{ $json.workflow.name }}
Error: {{ $json.execution.error.message }}
Execution ID: {{ $json.execution.id }}
- The error workflow activates automatically because it contains an Error Trigger node. No need to publish it separately.
Link the error workflow to your weather workflow
- Open your Weather Webhook workflow.
- Click the three dots menu (⋮) in the top right, then Settings.
- Under Error Workflow, select the
Error Handlerworkflow you just created. - Click Publish to apply the change.
Now if the weather API goes down or any node in your workflow throws an error during a production execution, you will get a Discord notification with the workflow name, error message, and execution ID. You can use that execution ID to find the failed run in the Executions tab and inspect exactly what went wrong.
This is a pattern you should reuse in every workflow you build. Create one Error Handler workflow, then link all your other workflows to it.
n8n expressions quick reference
Expressions are how you reference data from previous nodes. They live inside double curly braces {{ }} and are available in most node fields.
| Expression | What it does |
|---|---|
{{ $json.fieldName }} |
Access a field from the previous node's output |
{{ $json.nested.field }} |
Access nested JSON properties |
{{ $('NodeName').item.json.field }} |
Access a specific node's output by name |
{{ $json.current_condition[0].temp_C }} |
Access an array element |
{{ $now.toFormat('yyyy-MM-dd') }} |
Current date formatted with Luxon |
{{ $now.toFormat('HH:mm') }} |
Current time (24h format) |
{{ $if($json.score > 80, "high", "low") }} |
Conditional expression |
{{ $json['field with spaces'] }} |
Bracket notation for special characters |
n8n uses Luxon for date handling. If you need to format dates from API responses, Luxon methods are available on any DateTime object.
How do you build an RSS feed monitor workflow in n8n?
Now build a second workflow that runs on a schedule instead of a webhook. This one monitors an RSS feed and posts new articles to Discord.
Step 1: Create the workflow
Click Add workflow on the Workflows page. Name it RSS to Discord.
Step 2: Add the RSS Feed Trigger
Click the trigger placeholder and search for RSS Feed Trigger. Add it.
Configure it:
- Feed URL: Use any blog RSS feed. For example:
https://blog.n8n.io/rss/(n8n's own blog). - Poll Times: Set to Every X > Value:
30> Unit: Minutes.
This checks the feed every 30 minutes for new items. The RSS Feed Trigger remembers which items it has already seen. On the first run, it outputs all current items. After that, it only outputs new ones.
Click Fetch Test Event to pull current feed items. You should see entries with fields like title, link, contentSnippet, isoDate, and creator.
Step 3: Add a Set node to format the message
Add a Set node. Create one field:
| Field name | Type | Value (expression) |
|---|---|---|
message |
String | 📰 New post: **{{ $json.title }}**\n{{ $json.link }}\nPublished: {{ $json.isoDate }} |
The \n inserts line breaks in the Discord message. The ** around the title makes it bold in Discord's Markdown.
Step 4: Add the Discord node
Add a Discord node. Use the same webhook credential. Set the message to {{ $json.message }}.
Click Test step. A formatted blog post notification should appear in your Discord channel.
Step 5: Publish the workflow
Click Publish. The RSS Feed Trigger will now check the feed every 30 minutes and post new items to Discord.
Link the Error Handler: Open the workflow settings and set the Error Workflow to your Error Handler workflow. Publish again to apply the change.
Verification: Check the Executions tab after 30 minutes (or change the poll interval to 1 minute temporarily). You should see an execution logged, even if there were no new items (the workflow runs but produces no output if nothing is new).
How do you export and import n8n workflows?
Export your workflows as JSON to back them up, share them, or move them between n8n instances.
Export a workflow
- Open the workflow you want to export.
- Click the three dots menu (⋮) > Download.
- n8n saves a
.jsonfile to your computer.
You can also export from the command line if you have shell access to the n8n container:
docker exec n8n n8n export:workflow --id=<workflow-id> --output=/tmp/workflow.json
Import a workflow
- On the Workflows page, click the three dots menu > Import from file.
- Select your
.jsonfile. - n8n imports the workflow as a draft. Review it, update any credentials, then publish.
Note: Exported workflows do not include credentials. You will need to re-add them after importing on a new instance.
Troubleshooting
Webhook returns 404: The test URL is only active while you have "Listen for test event" open in the editor. The production URL is only active after you publish the workflow. Make sure you are using the right URL for the right mode.
Discord message not appearing: Verify the webhook URL in your Discord credential. Test it directly with curl:
curl -X POST "https://discord.com/api/webhooks/YOUR/WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d '{"content": "Test message from curl"}'
If this works but n8n does not, the issue is in your n8n Discord node configuration.
Expression returns undefined:
Open the expression editor (click the field, then the expression toggle). n8n shows available data from previous nodes. Check the exact field path. Common mistake: using $json.field when the data is nested in $json.data.field.
RSS Feed Trigger fires for all items on first run: This is expected. On the first execution after publishing, the trigger outputs all current feed items because it has no history of what it has already processed. Subsequent runs only output new items.
Error workflow not firing: Error workflows only trigger on published, automatically-executed workflows. Manual test executions do not trigger them. Verify the error workflow is linked in the main workflow's settings.
Check the logs: If something is wrong at the n8n application level, check the container logs:
docker logs n8n --tail 50
For ongoing monitoring:
docker logs n8n -f
Next steps
Two working workflows and an error handler. From here:
- Secure your webhook endpoints with authentication headers.
- Add AI processing to your workflows with Ollama or Claude.
- Explore the full guide for production hardening.
- Learn Docker Compose fundamentals for managing multi-service stacks. See Docker Compose for Multi-Service VPS Deployments.
Copyright 2026 Virtua.Cloud. All rights reserved. This content is original work by the Virtua.Cloud team. Reproduction, republication, or redistribution without written permission is prohibited.
Ready to try it yourself?
Deploy your own server in seconds. Linux, Windows, or FreeBSD.
See VPS Plans