n8n + Private AI: Build GDPR-Compliant Automation Workflows with JuiceFactory
n8n is powerful for automating business processes. Adding LLM capabilities makes it more powerful still. But if your workflows handle sensitive data — customer emails, contracts, internal documents — sending that data to public AI providers creates a compliance gap you probably don't want.
JuiceFactory solves this. It's an EU-hosted, GDPR-compliant inference API that speaks the OpenAI protocol. That means n8n's built-in OpenAI nodes work without modification. You change a URL and an API key, and your workflow data stays in the EU with zero retention on the inference side.
This guide walks through the full setup: credentials, your first workflow, RAG patterns, production-ready examples, and the GDPR details that matter for automation pipelines.
Why n8n + private inference
Keep workflow data in the EU
When n8n processes documents, customer communications, or HR data, you need to know where that data goes. With JuiceFactory, the AI inference step runs on EU infrastructure. No transatlantic data transfers, no adequacy decision debates, no supplementary measures.
Zero retention on inference
Public AI APIs receive your prompts and keep them — typically 30 days for abuse monitoring. JuiceFactory operates statelessly: your prompt goes in, the response comes out, nothing is stored. The AI node in your workflow doesn't become a data retention liability.
Predictable latency
Shared infrastructure means shared resources. During peak times on public APIs, latency can spike from 200ms to 2+ seconds. Dedicated EU infrastructure gives you consistent response times, which matters when your n8n workflows have SLAs or timeouts.
OpenAI compatibility = zero migration effort
If you already have n8n workflows using OpenAI nodes, switching to JuiceFactory is a credential change. Same request format, same response format, same node configuration. The only difference is where the inference happens.
Prerequisites
Before you start, you need three things:
1. A running n8n instance
Either self-hosted or n8n Cloud. Version 1.30+ recommended — earlier versions have quirks with custom OpenAI base URLs.
If you're self-hosting, Docker is the simplest path:
docker run -d \
--name n8n \
-p 5678:5678 \
-v n8n_data:/home/node/.n8n \
n8nio/n8n
2. A JuiceFactory API key
Sign up at portal.juicefactory.ai and generate an API key. Keys are prefixed with jf_ so they're easy to identify in your credential store.
3. Verify your API access
Before touching n8n, confirm the API responds:
curl -s https://api.juicefactory.ai/v1/models \
-H "Authorization: Bearer jf_your-api-key-here" | jq .
You should see a list of available models. If you get a 401, check your key. If you get a timeout, check your network — the endpoint must be reachable from wherever n8n is running.
You can also test a quick completion:
curl -s https://api.juicefactory.ai/v1/chat/completions \
-H "Authorization: Bearer jf_your-api-key-here" \
-H "Content-Type: application/json" \
-d '{
"model": "qwen3-30b-a3b",
"messages": [{"role": "user", "content": "Say hello in Swedish"}],
"max_tokens": 50
}' | jq .choices[0].message.content
If that returns a response, you're ready.
Step 1: Configure JuiceFactory credentials in n8n
n8n uses the "OpenAI" credential type for any OpenAI-compatible API. Here's how to set it up.
Add the credential
- In n8n, go to Credentials in the left sidebar
- Click Add Credential
- Search for OpenAI API and select it
- Fill in these fields:
| Field | Value |
|---|---|
| API Key | jf_your-api-key-here |
| Base URL | https://api.juicefactory.ai/v1 |
- Click Save
That's it. The base URL override tells n8n to send all OpenAI-format requests to JuiceFactory instead of OpenAI's servers.
Name the credential clearly
Call it something like JuiceFactory EU or JuiceFactory Production. When you have multiple credentials (maybe a staging key and a production key), clear naming prevents accidental data routing mistakes.
Test the credential
After saving, create a quick test workflow:
- Add a Manual Trigger node
- Add an OpenAI Chat Model node
- Select your new JuiceFactory credential
- Set the model to
qwen3-30b-a3b - In the prompt, type "Respond with OK"
- Execute the workflow
If you get "OK" back, the credential is working. If you get an error, check the troubleshooting section below.
Alternative: HTTP Header Auth
For more control (or if you want to hit endpoints beyond chat completions), you can use the HTTP Request node with Header Auth:
- Create a Header Auth credential
- Set Name to
Authorization - Set Value to
Bearer jf_your-api-key-here
This gives you raw access to any JuiceFactory endpoint from the HTTP Request node.
Step 2: Your first AI workflow — document summarization
Let's build something useful. This workflow takes a document URL, fetches the content, and returns an AI-generated summary.
Workflow overview
Manual Trigger → HTTP Request (fetch doc) → OpenAI Chat (summarize) → Set (format output)
Node-by-node configuration
Node 1: Manual Trigger
Nothing to configure. This is your entry point. In production, you'd replace this with a webhook, schedule, or another trigger.
Node 2: HTTP Request — fetch the document
| Setting | Value |
|---|---|
| Method | GET |
| URL | ={{ $json.documentUrl }} (or hardcode a URL for testing) |
| Response Format | Text |
For testing, hardcode a URL to a plain-text document or a simple web page.
Node 3: OpenAI Chat — summarize
| Setting | Value |
|---|---|
| Credential | Your JuiceFactory credential |
| Resource | Chat Message |
| Model | qwen3-30b-a3b |
| Prompt (System) | You are a document summarizer. Create a concise summary of the provided document. Focus on key facts, decisions, and action items. |
| Prompt (User) | ={{ $json.data }} |
Node 4: Set — format the output
Map the response to clean output fields:
| Setting | Value |
|---|---|
| summary | ={{ $json.message.content }} |
| source_url | ={{ $('HTTP Request').first().json.url }} |
| processed_at | ={{ $now.toISO() }} |
The workflow as importable JSON
Copy this into n8n via Import from JSON:
{
"nodes": [
{
"parameters": {},
"id": "trigger",
"name": "Manual Trigger",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [240, 300]
},
{
"parameters": {
"url": "https://example.com/sample-document.txt",
"options": {
"response": {
"response": {
"responseFormat": "text"
}
}
}
},
"id": "fetch-doc",
"name": "Fetch Document",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [460, 300]
},
{
"parameters": {
"model": "qwen3-30b-a3b",
"messages": {
"values": [
{
"role": "system",
"content": "You are a document summarizer. Create a concise summary. Focus on key facts, decisions, and action items. Return plain text, no markdown."
},
{
"role": "user",
"content": "={{ $json.data }}"
}
]
},
"options": {
"maxTokens": 500,
"temperature": 0.3
}
},
"id": "summarize",
"name": "Summarize",
"type": "@n8n/n8n-nodes-langchain.openAi",
"typeVersion": 1,
"position": [680, 300],
"credentials": {
"openAiApi": {
"id": "your-credential-id",
"name": "JuiceFactory EU"
}
}
},
{
"parameters": {
"assignments": {
"assignments": [
{
"name": "summary",
"value": "={{ $json.message.content }}"
},
{
"name": "processed_at",
"value": "={{ $now.toISO() }}"
}
]
}
},
"id": "format-output",
"name": "Format Output",
"type": "n8n-nodes-base.set",
"typeVersion": 3,
"position": [900, 300]
}
],
"connections": {
"Manual Trigger": {
"main": [[{"node": "Fetch Document", "type": "main", "index": 0}]]
},
"Fetch Document": {
"main": [[{"node": "Summarize", "type": "main", "index": 0}]]
},
"Summarize": {
"main": [[{"node": "Format Output", "type": "main", "index": 0}]]
}
}
}
Replace your-credential-id with the actual credential ID from your n8n instance after importing.
Step 3: RAG workflow — retrieve and generate
Retrieval-augmented generation (RAG) is where n8n + private AI gets genuinely useful. Instead of relying on the model's training data, you feed it relevant context from your own documents at query time.
This workflow accepts a question via webhook, retrieves relevant chunks from a vector database, builds a grounded prompt, and returns an answer.
Architecture
Webhook (question) → HTTP Request (query vector DB) → Code (build prompt) → OpenAI Chat (generate) → Respond to Webhook
Prerequisites for this workflow
You need a vector database with your documents already indexed. This example uses Qdrant because it's open-source and easy to self-host, but any vector DB with an HTTP API works.
If you don't have a vector DB yet, see our guide on RAG without a vector database for a simpler starting point.
Node configuration
Node 1: Webhook
| Setting | Value |
|---|---|
| HTTP Method | POST |
| Path | /ask |
| Response Mode | Last Node |
The webhook expects a JSON body like:
{
"question": "What is our refund policy for enterprise customers?"
}
Node 2: Get embedding for the question
Before querying the vector DB, you need an embedding of the question. Use an HTTP Request node to call JuiceFactory's embedding endpoint:
| Setting | Value |
|---|---|
| Method | POST |
| URL | https://api.juicefactory.ai/v1/embeddings |
| Authentication | Header Auth (your JuiceFactory credential) |
| Body (JSON) | See below |
{
"model": "qwen3-embed",
"input": "={{ $json.body.question }}"
}
Node 3: HTTP Request — query Qdrant
| Setting | Value |
|---|---|
| Method | POST |
| URL | http://your-qdrant-host:6333/collections/documents/points/search |
| Body (JSON) | See below |
{
"vector": "={{ $json.data[0].embedding }}",
"limit": 5,
"with_payload": true
}
This returns the 5 most relevant document chunks.
Node 4: Code — build the prompt
Use a Code node to assemble the retrieved chunks into a prompt:
const results = $input.first().json.result;
const question = $('Webhook').first().json.body.question;
const context = results
.map((r, i) => `[${i + 1}] ${r.payload.text}`)
.join('\n\n');
const systemPrompt = `You are a helpful assistant. Answer the user's question based ONLY on the provided context. If the context doesn't contain enough information, say so. Cite sources using [1], [2], etc.`;
const userPrompt = `Context:\n${context}\n\nQuestion: ${question}`;
return [{
json: {
systemPrompt,
userPrompt,
sourceCount: results.length
}
}];
Node 5: OpenAI Chat — generate answer
| Setting | Value |
|---|---|
| Credential | JuiceFactory EU |
| Model | qwen3-30b-a3b |
| System Message | ={{ $json.systemPrompt }} |
| User Message | ={{ $json.userPrompt }} |
| Temperature | 0.2 |
| Max Tokens | 800 |
Low temperature keeps the answer grounded in the retrieved context. Higher values increase the chance of hallucination.
Node 6: Respond to Webhook
Return the generated answer:
| Setting | Value |
|---|---|
| Response Body | JSON |
{
"answer": "={{ $json.message.content }}",
"sources_used": "={{ $('Build Prompt').first().json.sourceCount }}"
}
Testing the RAG workflow
Activate the workflow, then test from the command line:
curl -s -X POST https://your-n8n-instance.com/webhook/ask \
-H "Content-Type: application/json" \
-d '{"question": "What is our refund policy for enterprise customers?"}' | jq .
Advanced patterns
Error handling with retry and fallback
AI API calls can fail — rate limits, timeouts, transient errors. n8n's error handling is your friend here.
Retry on failure:
On the OpenAI Chat node, enable Retry on Fail:
| Setting | Value |
|---|---|
| Retry on Fail | Enabled |
| Max Retries | 3 |
| Wait Between Retries | 2000 ms |
This handles transient 500 errors and brief rate limit (429) responses.
Fallback with Error Trigger:
For more sophisticated handling, use an Error Trigger workflow:
Main Workflow:
Webhook → OpenAI Chat → Respond to Webhook
Error Workflow:
Error Trigger → IF (check error type) → [
Rate Limit → Wait 10s → Retry via HTTP Request
Timeout → Return cached/fallback response
Auth Error → Send Slack alert to ops team
]
Token counting for cost tracking
JuiceFactory returns token usage in the API response, just like OpenAI. Use a Code node after the OpenAI Chat node to extract and log it:
const response = $input.first().json;
const usage = response.usage || {};
return [{
json: {
content: response.message.content,
tokens_prompt: usage.prompt_tokens || 0,
tokens_completion: usage.completion_tokens || 0,
tokens_total: usage.total_tokens || 0,
estimated_cost_eur: ((usage.total_tokens || 0) / 1000) * 0.002
}
}];
Pipe these values into a Google Sheet, a database, or a monitoring tool to track usage over time. This is especially useful when multiple teams share a single API key.
Batch processing with SplitInBatches
When you need to process a list of items (emails, documents, rows from a spreadsheet), the SplitInBatches node prevents you from overwhelming the API:
Read Spreadsheet → SplitInBatches (batch size: 5) → OpenAI Chat → Merge results
Configuration on the SplitInBatches node:
| Setting | Value |
|---|---|
| Batch Size | 5 |
| Options → Reset | Enabled |
Add a Wait node after the OpenAI Chat node with a 500ms delay to stay well within rate limits. This is slower but reliable — you won't get 429s at 3 AM when nobody's watching.
Streaming responses — limitations in n8n
As of n8n 1.x, the OpenAI Chat node does not support streaming. The response arrives as a single block after generation completes. If you need streaming for a user-facing chat interface, use a direct WebSocket or SSE connection from your frontend to the JuiceFactory API, not n8n.
n8n is better suited for backend workflows where the response time is measured in "seconds are fine" rather than "first token latency matters."
Real workflow examples
1. Customer support triage — classify incoming emails
Problem: Your support inbox gets 200+ emails per day. You need to route them to the right team and flag urgent issues.
Workflow:
IMAP Trigger (new email) → OpenAI Chat (classify) → Switch (route by category) → [
billing → Jira (create ticket in Billing queue)
technical → Jira (create ticket in Engineering queue)
urgent → Jira (create P1 ticket) + Slack (alert on-call)
spam → Move to trash
]
Classification prompt (system message):
Classify the following customer email into exactly one category:
- billing (invoices, payments, pricing, subscription changes)
- technical (bugs, errors, integration help, API issues)
- urgent (service down, data loss, security incident)
- spam (marketing, unrelated, automated)
Also extract:
- customer_name: the sender's name if identifiable
- sentiment: positive, neutral, or negative
- summary: one sentence
Respond in JSON only. No explanation.
The Switch node routes based on $json.message.content parsed as JSON:
// In a Code node before the Switch:
const classification = JSON.parse($json.message.content);
return [{ json: classification }];
Then the Switch node checks {{ $json.category }} against the four values.
Why this needs private AI: Customer emails contain names, account details, contract terms, and sometimes personal data. Routing them through a US-based AI API means that data leaves the EU. With JuiceFactory, the classification happens on EU infrastructure with zero retention.
2. Invoice data extraction — PDF to structured JSON
Problem: Your accounting team receives invoices as PDF attachments. You need to extract key fields into your ERP system.
Workflow:
IMAP Trigger (new email with attachment) → Extract Attachment → HTTP Request (convert PDF to text) → OpenAI Chat (extract fields) → Code (validate) → HTTP Request (POST to ERP API)
Extraction prompt:
Extract the following fields from this invoice text. Return valid JSON only.
{
"vendor_name": "",
"invoice_number": "",
"invoice_date": "YYYY-MM-DD",
"due_date": "YYYY-MM-DD",
"currency": "EUR/USD/SEK/etc",
"line_items": [
{"description": "", "quantity": 0, "unit_price": 0.00, "total": 0.00}
],
"subtotal": 0.00,
"vat_rate": 0.00,
"vat_amount": 0.00,
"total_amount": 0.00,
"payment_reference": "",
"iban": ""
}
If a field is not found, use null. Do not guess values.
Validation Code node:
const extracted = JSON.parse($json.message.content);
// Basic validation
const errors = [];
if (!extracted.invoice_number) errors.push('Missing invoice number');
if (!extracted.total_amount) errors.push('Missing total amount');
if (extracted.total_amount < 0) errors.push('Negative total');
if (errors.length > 0) {
return [{
json: {
status: 'validation_failed',
errors,
raw: extracted
}
}];
}
return [{
json: {
status: 'valid',
...extracted
}
}];
Route validation failures to a manual review queue. Route valid extractions to the ERP API.
Why this needs private AI: Invoices contain vendor names, bank account numbers (IBAN), amounts, and business relationships. This is commercially sensitive data that belongs in your infrastructure, not in a training dataset.
3. Internal knowledge base Q&A — Slack to n8n to JuiceFactory to Slack
Problem: Your team asks the same questions repeatedly. You have documentation in Confluence/Notion/Google Drive but nobody reads it.
Workflow:
Slack Trigger (app mention) → Code (extract question) → HTTP Request (search docs) → Code (build RAG prompt) → OpenAI Chat (generate answer) → Slack (reply in thread)
Slack Trigger configuration:
Set up a Slack app with app_mentions:read scope. The trigger fires when someone @mentions the bot.
Code node — extract the question:
const event = $json;
const question = event.event.text.replace(/<@[A-Z0-9]+>/g, '').trim();
const channel = event.event.channel;
const threadTs = event.event.ts;
return [{
json: { question, channel, threadTs }
}];
Search node — query your doc index:
This is the same vector DB search pattern from the RAG workflow above. Adapt it to your documentation source.
Slack reply:
| Setting | Value |
|---|---|
| Channel | ={{ $('Extract Question').first().json.channel }} |
| Text | ={{ $json.message.content }} |
| Thread TS | ={{ $('Extract Question').first().json.threadTs }} |
Replying in the thread keeps the channel clean. The bot's answer appears directly under the question.
Why this needs private AI: Internal documentation often contains architecture details, security configurations, customer-specific information, and business strategy. Sending those to a public API as part of RAG context exposes proprietary information.
GDPR considerations for n8n workflows
Using JuiceFactory makes the AI inference step GDPR-compliant. But the workflow itself has its own data handling that you need to think about.
n8n execution logs contain personal data
Every workflow execution in n8n is logged. Those logs include the input data, the output data, and everything in between. If your workflow processes customer emails, the full email text is in the execution log.
What to do:
- Set execution log retention to the minimum you need. In n8n settings: Settings → Executions → Prune Executions
- For production, set retention to 7-30 days depending on your debugging needs
- For workflows that handle sensitive data, consider disabling execution saving entirely (
EXECUTIONS_DATA_SAVE_ON_ERROR=noneandEXECUTIONS_DATA_SAVE_ON_SUCCESS=nonein environment variables)
Self-hosted vs n8n Cloud data residency
| Deployment | Data location | Your control |
|---|---|---|
| Self-hosted (EU server) | Your EU infrastructure | Full |
| Self-hosted (non-EU) | Your chosen location | Full, but GDPR transfer rules apply |
| n8n Cloud (EU region) | n8n's EU infrastructure (GCP Frankfurt) | Moderate — n8n processes data on your behalf |
| n8n Cloud (US region) | US infrastructure | GDPR transfer mechanisms required |
For the strongest GDPR posture: self-host n8n on EU infrastructure and connect it to JuiceFactory. The entire pipeline — trigger, processing, inference, response — stays in the EU.
JuiceFactory's zero-retention makes the AI step clean
From a GDPR Article 28 perspective, the AI inference step with JuiceFactory is simple:
- No data processing agreement needed for stored data (there is no stored data)
- No data subject access requests to fulfill at the inference layer
- No retention schedules to track
- The data flow is: prompt in → response out → nothing retained
This means your GDPR effort focuses on n8n itself (execution logs, credential storage, workflow data) rather than on the AI provider.
Credential security
Your JuiceFactory API key gives access to inference. Treat it like a database password:
- In self-hosted n8n, credentials are encrypted at rest using
N8N_ENCRYPTION_KEY. Make sure you've set a strong key. - Never commit credentials to version control
- Rotate API keys periodically via the JuiceFactory portal
- Use separate API keys for development and production workflows
Troubleshooting
Common errors
401 Unauthorized
Error: Request failed with status code 401
Your API key is wrong or expired. Check:
- The key starts with
jf_ - No extra whitespace in the credential field
- The key is active in the JuiceFactory portal
429 Too Many Requests
Error: Request failed with status code 429
You've hit the rate limit. Options:
- Enable retry on the node (see Advanced Patterns above)
- Add a Wait node before the AI node in batch workflows
- Check your plan's rate limits in the portal
Timeout / ECONNREFUSED
Error: connect ECONNREFUSED
n8n can't reach the JuiceFactory API. Check:
- Your n8n instance has internet access (or network access to the API if using private endpoints)
- No firewall blocking outbound HTTPS on port 443
- DNS resolves
api.juicefactory.aifrom the n8n host
Test from the n8n host:
curl -v https://api.juicefactory.ai/v1/models \
-H "Authorization: Bearer jf_your-key"
Model not found
Error: Model 'gpt-4' not found
JuiceFactory hosts its own models, not OpenAI's. Check available models:
curl -s https://api.juicefactory.ai/v1/models \
-H "Authorization: Bearer jf_your-key" | jq '.data[].id'
Use one of the returned model IDs in your n8n node configuration.
How to test the API connection from n8n
If you're unsure whether n8n can reach JuiceFactory, add a temporary HTTP Request node:
- Create a new workflow with a Manual Trigger
- Add an HTTP Request node
- Method: GET
- URL:
https://api.juicefactory.ai/v1/models - Authentication: Header Auth with your JuiceFactory credential
- Execute
If this returns a JSON list of models, the connection is working. If it fails, the error message will tell you exactly what's wrong (DNS, firewall, auth).
Debug mode in n8n
When a workflow misbehaves, use n8n's built-in debugging:
-
Execution preview: Click on any past execution to see the data at each node. The input/output panels show exactly what was sent to and received from JuiceFactory.
-
Pin data: Pin the output of nodes upstream of the AI node, then re-run only the AI step. This avoids re-fetching documents or re-querying databases during debugging.
-
Expression editor: Use the expression editor on the OpenAI Chat node to inspect the exact prompt being sent. If the prompt is empty or malformed, you'll see it here.
-
Workflow execution list: Filter by status (error, success) to find failing runs. The error message on the failing node is usually sufficient to diagnose the issue.
Performance tips
Choose the right model for the task
Not every workflow needs the largest model. For classification and simple extraction, smaller models are faster and cheaper:
| Task | Recommended model | Why |
|---|---|---|
| Classification (email routing, sentiment) | qwen3-30b-a3b | Fast, accurate for structured output |
| Summarization | qwen3-30b-a3b | Good balance of speed and quality |
| Complex reasoning / RAG | qwen3-235b-a22b | Better at synthesis and nuanced answers |
| Embedding generation | qwen3-embed | Purpose-built for vector search |
Set max_tokens appropriately
Don't use the default max_tokens for every workflow. A classification task needs 50 tokens. A summary needs 500. A detailed analysis might need 2000. Setting this correctly reduces latency and cost:
Classification prompt → max_tokens: 100
Summary prompt → max_tokens: 500
Full analysis → max_tokens: 2000
Cache repeated queries
If your workflow processes the same type of input frequently (e.g., classifying emails into categories), consider caching results. Use n8n's Code node with a simple in-memory map, or query a Redis/database cache before hitting the AI API.
Related guides
- Migrate from OpenAI to an EU API — Two-line code change to switch from OpenAI to JuiceFactory in any application
- Stateless LLM APIs and GDPR — Deep dive into why zero-retention inference matters for compliance
- RAG without a vector database — Build a minimal RAG pipeline in Python before adding infrastructure
- BYOK AI — Bring Your Own Key — Use your own API keys with JuiceFactory's EU infrastructure
Next steps
You've got the building blocks: credentials configured, a working summarization workflow, a RAG pattern, and three production-ready examples to adapt.
The fastest way to start is to pick the example closest to your use case, import the workflow JSON, swap in your credentials and model, and test with real data.
If you're running n8n workflows that handle EU personal data and need the AI step to be compliant, get your JuiceFactory API key and you can have your first private AI workflow running in under 15 minutes.