n8n is an open-source workflow automation tool that self-hosts. Unlike Zapier or Make, you run it on your own server, pay nothing per workflow execution, and keep your data in your own infrastructure. For remote teams handling sensitive client data or running high-volume automations, n8n eliminates per-task pricing and data residency concerns.
This guide covers: self-hosted n8n setup, five practical remote team workflows, and error handling to make automations reliable.
Install n8n with Docker
# Production install with persistent storage and tunnel support
docker run -d \
--name n8n \
--restart unless-stopped \
-p 5678:5678 \
-v ~/.n8n:/home/node/.n8n \
-e N8N_BASIC_AUTH_ACTIVE=true \
-e N8N_BASIC_AUTH_USER=admin \
-e N8N_BASIC_AUTH_PASSWORD=changeme-secure-password \
-e N8N_HOST=automation.yourdomain.com \
-e N8N_PORT=5678 \
-e N8N_PROTOCOL=https \
-e WEBHOOK_URL=https://automation.yourdomain.com/ \
-e GENERIC_TIMEZONE=America/New_York \
n8nio/n8n:latest
# Access at https://automation.yourdomain.com
# Default editor runs on port 5678
# Docker Compose with PostgreSQL for larger teams
# n8n with SQLite is fine for single users; use Postgres for teams
version: "3.9"
services:
n8n:
image: n8nio/n8n:latest
restart: unless-stopped
ports:
- "5678:5678"
environment:
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_DATABASE=n8n
- DB_POSTGRESDB_USER=n8n
- DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
- N8N_BASIC_AUTH_ACTIVE=true
- N8N_BASIC_AUTH_USER=admin
- N8N_BASIC_AUTH_PASSWORD=${N8N_PASSWORD}
- WEBHOOK_URL=https://automation.yourdomain.com/
- GENERIC_TIMEZONE=UTC
volumes:
- n8n_data:/home/node/.n8n
depends_on:
- postgres
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
- POSTGRES_DB=n8n
- POSTGRES_USER=n8n
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
n8n_data: {}
postgres_data: {}
Workflow 1: GitHub PR to Slack Notification with Context
The default GitHub → Slack integration only sends a link. This workflow sends a formatted message with reviewer names, labels, and a direct link to the diff.
{
"name": "GitHub PR to Slack",
"nodes": [
{
"type": "n8n-nodes-base.webhook",
"name": "GitHub Webhook",
"parameters": {
"httpMethod": "POST",
"path": "github-pr",
"responseMode": "onReceived"
}
},
{
"type": "n8n-nodes-base.if",
"name": "Filter PR events",
"parameters": {
"conditions": {
"string": [
{
"value1": "={{ $json.body.action }}",
"operation": "equal",
"value2": "opened"
}
]
}
}
},
{
"type": "n8n-nodes-base.slack",
"name": "Send to Slack",
"parameters": {
"channel": "#engineering",
"text": "New PR: {{ $json.body.pull_request.title }}",
"attachments": [
{
"color": "#36a64f",
"fields": [
{
"title": "Author",
"value": "{{ $json.body.pull_request.user.login }}",
"short": true
},
{
"title": "Changes",
"value": "+{{ $json.body.pull_request.additions }} / -{{ $json.body.pull_request.deletions }}",
"short": true
}
],
"actions": [
{
"type": "button",
"text": "Review PR",
"url": "{{ $json.body.pull_request.html_url }}"
}
]
}
]
}
}
]
}
Workflow 2: Daily Standup Reminder with Auto-Summary
Trigger: Schedule — 9:00 AM Mon-Fri
Step 1: GitHub node — fetch commits from last 24 hours per developer
Step 2: Code node — format into standup summary
Step 3: Slack node — post to #standup channel
// Code node: format standup summary
const commits = items.flatMap(item => item.json.commits || []);
const byAuthor = commits.reduce((acc, commit) => {
const author = commit.author?.login || commit.commit.author.name;
if (!acc[author]) acc[author] = [];
acc[author].push(commit.commit.message.split('\n')[0]);
return acc;
}, {});
const message = Object.entries(byAuthor)
.map(([author, msgs]) => {
return `*${author}*:\n${msgs.map(m => `• ${m}`).join('\n')}`;
})
.join('\n\n');
return [{ json: { message: message || 'No commits in the last 24 hours.' } }];
Workflow 3: New Notion Page → Slack Alert
When anyone creates a new page in a specific Notion database (e.g., the Engineering Decisions database), notify the team immediately.
Trigger: Webhook (Notion webhook via Zapier or Notion API polling)
OR: Schedule — poll Notion API every 5 minutes
Step 1: HTTP Request — Notion API
GET https://api.notion.com/v1/databases/DATABASE_ID/query
Headers: Authorization: Bearer NOTION_TOKEN
Body: {
"filter": {
"property": "Created time",
"created_time": {
"after": "{{ $now.minus(5, 'minutes').toISO() }}"
}
}
}
Step 2: IF — results exist
conditions: {{ $json.results.length > 0 }}
Step 3: Split in Batches — one notification per new page
Step 4: Slack — post to #decisions
"New decision logged: {{ $json.properties.Name.title[0].text.content }}"
"Author: {{ $json.created_by.name }}"
"Link: {{ $json.url }}"
Workflow 4: Failed CI Build to Linear Issue
Automatically create a bug ticket when a CI build fails on main.
Trigger: Webhook (GitHub Actions calls this webhook on failure)
Step 1: HTTP Request — check if issue already exists in Linear
GET https://api.linear.app/graphql
Query: issues with title containing the workflow name
Step 2: IF — no duplicate issue
Step 3: HTTP Request — create Linear issue
POST https://api.linear.app/graphql
Headers: Authorization: YOUR_LINEAR_API_KEY
Body: {
"query": "mutation CreateIssue($input: IssueCreateInput!) { issueCreate(input: $input) { issue { id identifier } } }",
"variables": {
"input": {
"title": "CI Failed: {{ $json.body.workflow }} on main",
"description": "**Branch**: main\n**Commit**: {{ $json.body.commit_sha }}\n**Workflow**: [View run]({{ $json.body.run_url }})",
"teamId": "YOUR_TEAM_ID",
"priority": 2,
"labelIds": ["BUG_LABEL_ID"]
}
}
}
# In your GitHub Actions workflow, call the n8n webhook on failure
- name: Notify n8n on failure
if: failure()
run: |
curl -X POST https://automation.yourdomain.com/webhook/ci-failure \
-H "Content-Type: application/json" \
-d '{
"workflow": "${{ github.workflow }}",
"commit_sha": "${{ github.sha }}",
"run_url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}'
Workflow 5: Weekly Team Metrics Digest
Trigger: Schedule — Friday 5:00 PM
Step 1: GitHub — fetch open PRs older than 48 hours
GET /repos/:owner/:repo/pulls?state=open
Step 2: GitHub — fetch merged PRs this week
GET /repos/:owner/:repo/pulls?state=closed&since=<monday_date>
Step 3: Linear — fetch completed issues this week
GraphQL query with date filter
Step 4: Code node — format weekly digest message
Step 5: Slack — post to #engineering-metrics
Error Handling for Reliable Workflows
Workflows fail silently without error handling. Add error notifications:
For any workflow:
1. Click "Add Error Workflow" in workflow settings
2. Create a separate "Error Handler" workflow that:
- Receives the failed workflow's name and error
- Posts to Slack #automation-errors
- Logs to a Notion database for tracking
// In the Error Handler workflow's Code node
const error = $input.first().json;
return [{
json: {
message: `Automation failed: *${error.workflow.name}*`,
details: [
`Error: ${error.execution.error?.message || 'Unknown'}`,
`Node: ${error.execution.lastNodeExecuted || 'Unknown'}`,
`Time: ${new Date(error.execution.startedAt).toISOString()}`,
`<https://automation.yourdomain.com/workflow/${error.workflow.id}|View Workflow>`,
].join('\n'),
}
}];
Manage Credentials Securely
n8n stores credentials encrypted in its database. For team environments, avoid hardcoding API keys in workflow nodes:
n8n → Settings → Credentials → Add Credential
- GitHub API: Personal Access Token
- Slack API: Bot Token
- Notion API: Integration Token
- Linear API: API Key
Then reference credentials by name in workflow nodes — never paste raw API keys into node parameters.
Related Reading
- Automation Tools for Freelance Business Operations
- Async Standup Alternative Using GitHub Commit Summaries Automatically
- CI/CD Pipeline for Solo Developers: GitHub Actions
Built by theluckystrike — More at zovo.one