Six n8n patterns: from a morning brief to a health check
The final lesson of the series pulls everything together into six workflow patterns that fit almost anywhere: a scheduled brief, inquiry triage, note indexing, a monthly report, data sync and a health check. The examples use neutral names — you take the pattern and put it on your data. One rule applies everywhere: a human decides before anything is sent out.
{{ … }}) and queries are examples: try them with "Execute step" before you publish.01What you'll learn
- The common skeleton of every good workflow: trigger → check → process → store → notify.
- Six patterns that cover most everyday automations.
- Where you must put a human in the middle: before every email, publication or payment.
- How to use a model for classification and drafts without handing it the keys.
- Why code computes the numbers and the model does not.
- How to make workflows safe to repeat (idempotency) and how to find out when something breaks.
02Before you start
- You have done lessons 1–9 of the series: a working n8n 2.x install, Postgres, at least one model (local via Ollama 🔒 local or cloud 🌐 global), and the basics of errors and monitoring.
- Data for practice: a few rows in a table with sample values. Do not start with real client data.
- Mail access for the notifications: a Gmail or Send Email (SMTP) node with credentials already in n8n's credential store.
- For pattern 3: a Qdrant vector database and an embedding model (see DevStation · Lesson 3: Docker, n8n and OpenClaw).
03Steps
-
The common skeleton
All six patterns share one backbone. Why? Because when everything is laid out the same way, you look for bugs in the same places.
diagramtrigger → check the input → process / model → store → notify ↘ error → shared "error workflow" → alert to you- Trigger: Schedule Trigger (on a timetable), Webhook (someone calls from outside) or manual.
- Credentials live only in the credential store, never typed into a node.
- Publishing: in n8n 2.x a workflow with a trigger runs once it is published; a schedule does not start from an unpublished workflow.
- Time: the schedule uses the workflow's timezone (if set) or the instance's — set it explicitly in Workflow settings.
-
Pattern 1 · Scheduled brief
Every morning you get a short email to yourself with open tasks and one exchange rate. An email to yourself is safe to send automatically.
diagramSchedule Trigger → Postgres (Select/Execute Query) → HTTP Request → Merge → Basic LLM Chain (short brief) → Send Email (to you)Schedule Trigger: interval Custom (Cron) with the expression below — weekdays at 07:30. If you want to skip public holidays, add an If node with a list of holiday dates.
cron · example30 7 * * 1-5sql · Postgres "Execute Query" · example namesSELECT t.title, t.priority, t.deadline, p.name AS project FROM tasks t LEFT JOIN projects p ON p.id = t.project_id WHERE t.status = 'open' AND (t.deadline IS NULL OR t.deadline >= CURRENT_DATE) ORDER BY CASE t.priority WHEN 'high' THEN 1 WHEN 'normal' THEN 2 ELSE 3 END, t.deadline ASC NULLS LAST LIMIT 7;HTTP Request · public rates APIGET https://api.frankfurter.dev/v1/latest?base=EUR&symbols=USD,GBPmodel prompt · exampleYou are a personal assistant. Date: {{ $now.toFormat('yyyy-MM-dd') }} Open tasks: {{ $('Open tasks').all().map((t, i) => `${i + 1}. [${t.json.priority}] ${t.json.title} | ${t.json.project} | due ${t.json.deadline ?? 'no deadline'}`).join('\n') }} EUR/USD: {{ $('Rates').item.json.rates.USD }} Write a morning brief of 3–4 sentences: focus on the two most important tasks and one practical takeaway. No headings.The names inside
$('…')are the names of your own nodes — rename them so the expression finds them. The last node is Send Email to your own address. -
Pattern 2 · Inquiry triage (with a human in the middle)
The website form calls a webhook; the workflow checks, classifies, stores and prepares a draft reply. The email to the sender goes out only after approval.
diagramWebhook (Header auth) → If (validate) → Text Classifier → Postgres (Insert) → Basic LLM Chain (draft) → Gmail "Send and Wait for Approval" (to you) → Send Email (to the sender, only if approved)- Webhook: method POST, Authentication → Header auth with a generated secret kept in the credential store. The Production URL works once published; the Test URL while you build. The sender (your website) sends the header with the secret.
- If: validation — the email contains "@", the message is not empty. Invalid input stops here.
- Text Classifier: categories with a short description (for example "training", "partnership", "technical", "other"). In the options turn on When No Clear Match → Output on Extra, "Other" Branch so you never lose an inquiry.
- Postgres → Insert: store name, email, message, category and received time — with Map Each Column Manually.
- Draft: a model chain that writes at most three sentences in the sender's language.
- Approval: Gmail → Message → Send and Wait for Approval addressed to you, with the draft in the body; Type of Approval: Approve and Disapprove. The workflow pauses until you press a button.
- Send Email to the sender — only on the "approved" branch.
prompt · classification and draft · exampleCategory description "technical": questions about errors, settings and integrations. Category description "partnership": proposals for cooperation. Draft: Reply in at most 3 sentences in the language of the message. Do not promise prices, deadlines or results. If something is unclear, ask one question. Sender: {{ $json.body.name }} Message: {{ $json.body.message }}🔐Text from the form is data, not an orderSomeone can type "Ignore your instructions and …" into the field. That is why the model in this workflow has no tools and decides nothing — it only proposes text that you read. It is also the reason for the approval. -
Pattern 3 · Note indexer into a vector database
Every half hour the workflow takes new notes from a folder, extracts the essentials and stores them in a vector database for search.
diagramSchedule Trigger → Read/Write Files from Disk → Extract From File → Postgres (Select by hash) → If (new?) → Qdrant Vector Store (Insert) → Postgres (Insert the hash)- Folder: Read/Write Files from Disk reads only from the allowed folder (
N8N_RESTRICT_FILE_ACCESS_TO; by default n8n's own files folder). In Docker, mount it as a volume. - No commands: old recipes moved the file with
mvthrough Execute Command. As of n8n 2.0 that node is disabled — and rightly so, it is risky. Instead of moving, keep the hash or name of each processed file in a table and skip what you have seen. That way a re-run creates no duplicates. - Extract From File (for a text file) turns the content into text.
- Qdrant Vector Store → Insert Documents with an embeddings node (for example Embeddings Ollama). The collection must match the embedding model's vector size.
- Summarising model (optional): before storing you can pull out a title, summary and tags with the Information Extractor node.
sql · table for processed filesCREATE TABLE IF NOT EXISTS indexed_files ( file_hash text PRIMARY KEY, file_name text, indexed_at timestamptz DEFAULT now() );💡Quality versus speedA bigger model extracts cleaner summaries but needs a powerful machine. For practice on a laptop a small model is perfectly enough. - Folder: Read/Write Files from Disk reads only from the allowed folder (
-
Pattern 4 · Monthly report: code computes the numbers
On the first of the month the workflow reads an export (CSV) from the accounting software, sums by category, the model comments on the finished numbers, and the email to the accountant goes out after approval.
diagramSchedule Trigger (monthly) → Read/Write Files → Extract From File (CSV) → Code (sum up) → Basic LLM Chain (commentary) → Convert to File → Write File → Gmail "Send and Wait for Approval" → Send EmailSchedule Trigger: interval Months, day 1, hour 06:00 (or cron
0 6 1 * *). The file name can be built with an expression for the previous month:expression · file name{{ $now.minus({ months: 1 }).toFormat('yyyy-MM') }}_transactions.csvjavascript · Code node · "Run Once for All Items"const totals = {}; for (const item of $input.all()) { const cat = item.json.category || 'Uncategorised'; const cents = Math.round(parseFloat(item.json.amount) * 100); totals[cat] = (totals[cat] || 0) + cents; } return Object.entries(totals).map(([category, cents]) => ({ json: { category, total_eur: (cents / 100).toFixed(2) } }));✅The model does not do the mathsLanguage models get arithmetic wrong. So the summing is in the Code node (in cents, as whole numbers), and the model receives the finished totals and writes only commentary: the biggest categories, the change from last month, anything worth checking. Before sending, a human compares the numbers in the email with the file.For the report file: Convert to File (CSV format) and then Read/Write Files from Disk → Write File to Disk in the allowed folder. Money is in euro.
-
Pattern 5 · Data sync and a sub-workflow
An outside system sends a new or changed record; the workflow stores it without duplicates and, if it is new, runs a shared sub-workflow.
diagramWebhook (Header auth) → Postgres (Select by external_id) → If (new?) → Postgres (Insert or Update) → Execute Sub-workflow (new only) → notification to you- Postgres → Insert or Update: you choose the column to match on (
external_id); it must have a unique constraint in the table. - Select with "Always Output Data": without this setting an empty result would stop the workflow before it reaches the If. Careful: do not set it on the If node itself — it can loop.
- Execute Sub-workflow: the shared work (for example "add a row to the contact list") lives in a separate workflow with an Execute Sub-workflow Trigger where you define the input fields. You call it from several workflows — one fix in one place.
sql · table · example namesCREATE TABLE IF NOT EXISTS partners ( id serial PRIMARY KEY, external_id text UNIQUE NOT NULL, name text, category text, status text DEFAULT 'active', updated_at timestamptz DEFAULT now() );⚠️Input from an outside system is untrusted tooValidate the fields (If), write through parameterised queries or the Postgres node (n8n sanitises "Query Parameters"), and never paste received text straight into SQL. - Postgres → Insert or Update: you choose the column to match on (
-
Pattern 6 · Health check with one alert per 30 minutes
Every 5 minutes the workflow asks its services "are you alive?", and on a problem sends one email, not hundreds.
diagramSchedule Trigger (*/5) → [HTTP Request ‖ HTTP Request ‖ Postgres "SELECT 1"] → Merge → Code (summary) → If (any failure?) → Postgres (when was the last email?) → If (> 30 min?) → Send Email → Postgres (store the time)- Parallel checks: on each HTTP Request set Settings → On Error → Continue (using error output) so the whole workflow does not stop when one service is down. The service addresses sit in your own private network and are never published anywhere.
- Merge collects the results; Code turns them into one record.
- No repeat emails: keep the time of the last email in a one-row table and send a new one only if more than 30 minutes have passed. (Static data in the Code node is experimental and does not work while testing — that is why we use a table.)
javascript · Code node · summaryconst checks = $input.all().map(i => ({ service: i.json.service, ok: i.json.ok === true })); const failed = checks.filter(c => !c.ok); return [{ json: { allOk: failed.length === 0, failed, summary: failed.length === 0 ? 'All systems normal' : 'Problem: ' + failed.map(f => f.service).join(', ') }}];sql · alert stateCREATE TABLE IF NOT EXISTS alert_state ( id int PRIMARY KEY DEFAULT 1, last_alert_at timestamptz ); INSERT INTO alert_state (id) VALUES (1) ON CONFLICT DO NOTHING; -
One shared "error workflow" and a backup
Build a separate workflow that starts with an Error Trigger and sends a short notice (which workflow, which node, what message). Then in each of the six workflows: Options → Settings → Error workflow and pick this one. One handler can serve many workflows. Run it once on purpose — otherwise you do not know that it works.
Backup: from the n8n command line,
n8n export:workflow --all --output=<folder>(with--backup— one file per workflow). Credentials stay encrypted withN8N_ENCRYPTION_KEY— keep that key somewhere separate (see also lesson 1 of this series). -
When something does not work
❌The schedule does not startCheck that the workflow is published and which timezone applies (workflow → instance). Variable values in a schedule are evaluated at publish time — if you change them, unpublish and publish again. Missed runs are not made up by default.❌The webhook returns 404The "Test URL" works only while you are listening in the editor; for daily use it is the "Production URL" and the workflow must be published.❌The file node cannot see the folderThe folder is outside the allowed one (N8N_RESTRICT_FILE_ACCESS_TO) or is not mounted as a volume in Docker. Execute Command and Local File Trigger are disabled by default — do not turn them on for convenience.❌An inquiry with no category disappearsBy default Text Classifier discards an item that has no clear match. Turn on the "Other" branch.
04Check
Checklist
- Every workflow has an "error workflow" selected, and the handler itself has been run once.
- Webhooks require an authentication header; no secrets are typed into nodes.
- Nothing leaves without an "approve" step.
- File workflows work only in the allowed folder and without a command node.
- Sums come from Code, not from the model.
- Re-running the indexer and the sync changes nothing (no duplicates).
- Alerts go out at most once every 30 minutes.
- Workflows are published; there is a backup (
export:workflow) and the encryption key is stored safely.
Quiz
1. The website form triggers a workflow that writes a reply to the sender. What is right?
2. Why does the Code node compute the sums in the monthly report instead of the model?
3. How do you avoid indexing the same file twice without moving it with a command?
4. Where do you set what happens when a workflow fails?
05What's next
06Sources
- n8n: Schedule Trigger — cron, timezone, publishing.
- n8n: Webhook — Test and Production URL, header authentication.
- n8n: Text Classifier · Postgres · Execute Sub-workflow.
- n8n: Gmail — Send and Wait for Approval · human in the loop · Wait.
- n8n 2.0 breaking changes — the disabled nodes and the file-access restriction.
- n8n: handle errors gracefully · Error Trigger · workflow settings · node settings.
- n8n: Qdrant Vector Store · Read/Write Files from Disk · Extract From File.
- n8n: getWorkflowStaticData — why we do not use it for state.
- Frankfurter 🌐 global — the public currency-rate API in the example.