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

  1. In n8n, go to Credentials in the left sidebar
  2. Click Add Credential
  3. Search for OpenAI API and select it
  4. Fill in these fields:
FieldValue
API Keyjf_your-api-key-here
Base URLhttps://api.juicefactory.ai/v1
  1. 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:

  1. Add a Manual Trigger node
  2. Add an OpenAI Chat Model node
  3. Select your new JuiceFactory credential
  4. Set the model to qwen3-30b-a3b
  5. In the prompt, type "Respond with OK"
  6. 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:

  1. Create a Header Auth credential
  2. Set Name to Authorization
  3. 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

SettingValue
MethodGET
URL={{ $json.documentUrl }} (or hardcode a URL for testing)
Response FormatText

For testing, hardcode a URL to a plain-text document or a simple web page.

Node 3: OpenAI Chat — summarize

SettingValue
CredentialYour JuiceFactory credential
ResourceChat Message
Modelqwen3-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:

SettingValue
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

SettingValue
HTTP MethodPOST
Path/ask
Response ModeLast 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:

SettingValue
MethodPOST
URLhttps://api.juicefactory.ai/v1/embeddings
AuthenticationHeader Auth (your JuiceFactory credential)
Body (JSON)See below
{
  "model": "qwen3-embed",
  "input": "={{ $json.body.question }}"
}

Node 3: HTTP Request — query Qdrant

SettingValue
MethodPOST
URLhttp://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

SettingValue
CredentialJuiceFactory EU
Modelqwen3-30b-a3b
System Message={{ $json.systemPrompt }}
User Message={{ $json.userPrompt }}
Temperature0.2
Max Tokens800

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:

SettingValue
Response BodyJSON
{
  "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:

SettingValue
Retry on FailEnabled
Max Retries3
Wait Between Retries2000 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:

SettingValue
Batch Size5
Options → ResetEnabled

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:

SettingValue
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=none and EXECUTIONS_DATA_SAVE_ON_SUCCESS=none in environment variables)

Self-hosted vs n8n Cloud data residency

DeploymentData locationYour control
Self-hosted (EU server)Your EU infrastructureFull
Self-hosted (non-EU)Your chosen locationFull, 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 infrastructureGDPR 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.ai from 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:

  1. Create a new workflow with a Manual Trigger
  2. Add an HTTP Request node
  3. Method: GET
  4. URL: https://api.juicefactory.ai/v1/models
  5. Authentication: Header Auth with your JuiceFactory credential
  6. 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:

  1. 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.

  2. 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.

  3. 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.

  4. 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:

TaskRecommended modelWhy
Classification (email routing, sentiment)qwen3-30b-a3bFast, accurate for structured output
Summarizationqwen3-30b-a3bGood balance of speed and quality
Complex reasoning / RAGqwen3-235b-a22bBetter at synthesis and nuanced answers
Embedding generationqwen3-embedPurpose-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


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.

Connect Your Tools to EU AI in 5 Minutes

Works with Cursor, n8n, Continue.dev, and any OpenAI-compatible tool. Free tier included.