n8n outbound pattern: Forge LLM → n8n → external services¶
Forge agents (Claude Code, dispatcher workers) call n8n workflows when they need to act on an OAuth-gated service (Gmail, Calendar, Notion, TickTick, etc.). All OAuth tokens live in n8n's encrypted credentials store. Agents never hold tokens directly.
This is the outbound direction. The inbound direction (external service → n8n → forge task queue) is documented in README.md in this same folder.
Trust model¶
Claude Code (agent)
│
│ HTTP POST /webhook/<name>
│ Header: X-Forge-Auth: <secret>
▼
n8n workflow (single-purpose, narrowly scoped)
│
▼
External API (Gmail / Calendar / Notion / etc.)
The agent's only door into n8n is the set of webhooks that exist. Each webhook does one thing. There is no "send-email" webhook unless we deliberately build one. A webhook that creates a Gmail draft cannot send. Prompt injection has no path beyond the workflows we've explicitly defined.
Auth¶
A shared secret lives in two places:
| Location | Purpose |
|---|---|
~/.forge-secrets/n8n.env on UDev |
Read by scripts/forge_n8n_call.sh to set the X-Forge-Auth header |
FORGE_WEBHOOK_SECRET env var inside the n8n container |
Read by every workflow's first node to validate the header |
The n8n compose at /opt/n8n/docker-compose.yml (on CT 106) sets FORGE_WEBHOOK_SECRET and also N8N_BLOCK_ENV_ACCESS_IN_NODE=false so Code nodes can read $env.FORGE_WEBHOOK_SECRET.
Rotation: regenerate, update both places, restart the n8n container.
How agents call workflows¶
Always go through scripts/forge_n8n_call.sh:
Examples (workflows we'll build):
scripts/forge_n8n_call.sh forge-hello '{"msg":"ping"}'
scripts/forge_n8n_call.sh create-calendar-event '{"calendar":"personal","title":"Lunch with Brian","start":"2026-04-28T12:00:00-05:00","end":"2026-04-28T13:00:00-05:00","location":"Caroline's"}'
scripts/forge_n8n_call.sh create-gmail-draft '{"account":"[email protected]","to":"[email protected]","subject":"...","body":"..."}'
scripts/forge_n8n_call.sh create-notion-project '{"brand":"JustinWieb-VR","title":"Steam Announcement","date":"2026-04-27"}'
scripts/forge_n8n_call.sh create-ticktick-task '{"list":"today","title":"Buy flowers","date":"2026-04-30"}'
scripts/forge_n8n_call.sh read-emails '{"account":"personal","query":"is:unread newer_than:1d","max":20}'
The script:
- Reads N8N_BASE_URL and N8N_WEBHOOK_SECRET from ~/.forge-secrets/n8n.env
- POSTs to <N8N_BASE_URL>/webhook/<webhook-path> with the auth header
- Prints the workflow's JSON response to stdout
- Exits non-zero on HTTP error
URL choice, internal LAN only¶
Forge agents on UDev call n8n at http://192.168.86.82:5678. We do not use https://n8n.justinsforge.com for outbound calls. Reasons:
- Faster (no Cloudflare hop)
- Stays on the LAN, no surface to internet
- Independent of Cloudflare Access status
If a future agent runs off UDev and needs outbound calls, we add Tailscale or VPN, never the public hostname.
Workflow conventions¶
Every forge-outbound workflow follows the same shape:
- Webhook trigger: POST, path matches the workflow name, response mode
responseNode - Code node named "Validate + Echo" (or just "Validate"), checks
headers['x-forge-auth']against$env.FORGE_WEBHOOK_SECRET. Returns{ ok:false, error:'unauthorized' }if mismatch. - Action nodes, the actual work (Gmail Draft create, Calendar Insert, Notion Create, etc.)
- Respond to Webhook node, returns JSON
{ ok:true, ...result }on success,{ ok:false, error:... }on failure
Naming:
- Webhook path = workflow name = kebab-case, prefixed with the verb: create-, read-, update-, delete-
- Tag every forge workflow with forge and one of outbound / inbound
Source of truth¶
All forge workflow definitions live in infra/n8n/workflows/*.json in this repo, exported from n8n via the UI. The repo is the source of truth. If a workflow is changed live in n8n, immediately re-export and commit. If a workflow is deleted in n8n by accident, restore by importing the JSON.
Importing a workflow¶
In the n8n UI:
- Workflows tab → kebab menu (
⋯) → Import from File - Pick the JSON from
infra/n8n/workflows/ - Save → Activate
Adding a new workflow¶
- Build it in the n8n UI (UI is much easier than hand-rolling JSON)
- Test it from CLI:
scripts/forge_n8n_call.sh <path> '<payload>' - Export: workflow page → kebab menu → Download → save to
infra/n8n/workflows/<name>.json - Commit with the same name as the workflow
- Update this doc's example list if it's a generally-useful workflow
Phase 1 hardening: COMPLETE 2026-04-27¶
- Cloudflare Zero Trust Access in front of
n8n.justinsforge.com, email auth →[email protected] - n8n encryption key offsite (NordPass)
- n8n moved off justinwieb.com to
n8n.justinsforge.comvia media-server tunnel (forge-n8n tunnel + cloudflared on n8n LXC retired) - n8n login MFA enabled
- Webhook secret stored in NordPass
Phase 0 (account-level hardening on the two Gmail accounts) is the next gate before OAuth credentials go into n8n.