Services

automation / running

n8n

Workflow automation. Currently runs the Watchtower → LLM → Discord update-advisor pipeline.

What it is

n8n is a node-based workflow automation tool — think Zapier or Make, but self-hosted and with a workflow language that actually exposes code nodes for when the visual editor isn't enough. Workflows are triggered by webhooks, schedules, or polling, and chain together HTTP requests, transformations, conditional logic, and integrations with hundreds of services.

Why I run it

I have one big use case for it right now: the Watchtower update advisor pipeline. Watchtower sends "container X has a new image" notifications to an n8n webhook, n8n parses the report, looks up the GitHub release notes, hands everything to a local LLM for risk analysis, and posts a structured Discord embed with SAFE / REVIEW / WAIT verdicts.

That workflow could have been a Python script. It's an n8n workflow instead because:

Once n8n is in place for one workflow, the next is almost free — that's the bet.

How I use it

The Watchtower advisor is the only production workflow at the moment. It runs every time any Docker LXC's Watchtower poll finds an update, deduplicates by image hash with a 30-day cache, and only sends Discord notifications for SAFE or REVIEW verdicts (WAIT is silent — no point waking me up for a release the model itself thinks isn't worth acting on).

The LLM call is an HTTP request to LM Studio's OpenAI-compatible endpoint, with the model name swappable via a single field. The model gets a strict system prompt that forbids inventing release notes or recommending automatic updates. Output is a single JSON object the workflow parses for the Discord embed.

The trickiest plumbing detail was the LLM payload. Building it as a JSON string inline blew up — GitHub release notes contain literal newlines and quotes, and n8n's expression evaluator substituting those directly into a string produced invalid JSON. The fix is a Code node that constructs the payload as a real JavaScript object, then the next node JSON.stringifys it. Untrusted multiline text has to flow through JSON.stringify, not through string interpolation.

Setup notes

Runbook