AI & LLM

Building AI Agents with n8n and OpenAI: Complete Guide [2025]

House of Loops TeamJuly 25, 202512 min read
Building AI Agents with n8n and OpenAI: Complete Guide [2025]

Building AI Agents with n8n and OpenAI: Complete Guide

AI agents are transforming how we build automation workflows. Unlike simple chatbots, AI agents can reason, make decisions, use tools, and execute complex multi-step tasks autonomously. In this comprehensive guide, we'll build a production-ready AI agent using n8n and OpenAI's GPT-4.

What Are AI Agents?

AI agents go beyond basic prompt-response patterns. They can:

  • Reason and plan multi-step solutions
  • Use tools like APIs, databases, and external services
  • Maintain context across conversations
  • Make autonomous decisions based on goals
  • Handle errors and adapt their approach

Think of an AI agent as an autonomous worker that can understand goals, break them down into tasks, execute those tasks using available tools, and iteratively work toward completion.

Architecture Overview

Here's the high-level architecture we'll implement:

┌─────────────────────────────────────────────────────────┐
│                    User Interface                        │
│              (Webhook, Chat, Email, etc.)               │
└────────────────────┬────────────────────────────────────┘
                     │
┌────────────────────▼────────────────────────────────────┐
│                 n8n Workflow Engine                      │
│  ┌──────────────────────────────────────────────────┐  │
│  │         Agent Orchestration Layer                │  │
│  │  • Context Management                            │  │
│  │  • Tool Selection                                │  │
│  │  • Error Handling                                │  │
│  └──────────────────────────────────────────────────┘  │
│                                                          │
│  ┌──────────────────────────────────────────────────┐  │
│  │              OpenAI Integration                  │  │
│  │  • GPT-4 for reasoning                           │  │
│  │  • Function calling                              │  │
│  │  • Embeddings for memory                         │  │
│  └──────────────────────────────────────────────────┘  │
│                                                          │
│  ┌──────────────────────────────────────────────────┐  │
│  │                Tool Registry                     │  │
│  │  • API integrations                              │  │
│  │  • Database queries                              │  │
│  │  • Custom functions                              │  │
│  └──────────────────────────────────────────────────┘  │
└─────────────────────┬───────────────────────────────────┘
                      │
┌─────────────────────▼───────────────────────────────────┐
│              External Services & Data                    │
│  • Vector Database (Pinecone/Qdrant)                    │
│  • Supabase for state management                        │
│  • Third-party APIs                                     │
└─────────────────────────────────────────────────────────┘

Prerequisites

Before we begin, ensure you have:

  1. n8n instance (self-hosted or cloud)
  2. OpenAI API key with GPT-4 access
  3. Supabase account (for state management)
  4. Vector database (Pinecone or Qdrant for memory)
  5. Basic understanding of JavaScript/JSON

Step 1: Setting Up the Foundation

Configure n8n Environment Variables

Add these to your n8n environment:

# OpenAI Configuration
OPENAI_API_KEY=sk-your-api-key-here
OPENAI_MODEL=gpt-4-turbo-preview

# Supabase for State Management
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_KEY=your-anon-key

# Vector Database
PINECONE_API_KEY=your-pinecone-key
PINECONE_ENVIRONMENT=us-west1-gcp
PINECONE_INDEX=agent-memory

Create the Main Agent Workflow

Start with a webhook trigger that accepts user messages:

  1. Add a Webhook node as the entry point
  2. Set the HTTP method to POST
  3. Configure the response mode to "Wait for Webhook Response"

Step 2: Implementing Context Management

Context is crucial for AI agents. We'll use Supabase to maintain conversation state and history.

Supabase Schema

Create a table for conversation context:

CREATE TABLE agent_conversations (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  session_id VARCHAR(255) UNIQUE NOT NULL,
  user_id VARCHAR(255),
  context JSONB DEFAULT '{}'::jsonb,
  messages JSONB[] DEFAULT ARRAY[]::JSONB[],
  metadata JSONB DEFAULT '{}'::jsonb,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_session_id ON agent_conversations(session_id);
CREATE INDEX idx_user_id ON agent_conversations(user_id);

Load Context in n8n

Add a Supabase node after the webhook:

{
  "operation": "get",
  "table": "agent_conversations",
  "filters": {
    "session_id": "={{ $json.body.session_id }}"
  }
}

If no context exists, initialize it with a Function node:

// Initialize new conversation context
const sessionId = $input.item.json.body.session_id;
const userId = $input.item.json.body.user_id;

return {
  session_id: sessionId,
  user_id: userId,
  context: {
    goals: [],
    completed_tasks: [],
    pending_tasks: [],
    tool_usage: [],
  },
  messages: [],
  metadata: {
    started_at: new Date().toISOString(),
    total_interactions: 0,
  },
};

Step 3: Building the Tool Registry

Tools are functions the agent can call to perform actions. We'll implement several core tools:

Tool Definition Structure

Define tools in a format OpenAI's function calling understands:

const tools = [
  {
    name: 'search_knowledge_base',
    description: 'Search the vector database for relevant information',
    parameters: {
      type: 'object',
      properties: {
        query: {
          type: 'string',
          description: 'The search query',
        },
        limit: {
          type: 'number',
          description: 'Maximum number of results',
          default: 5,
        },
      },
      required: ['query'],
    },
  },
  {
    name: 'create_task',
    description: 'Create a new task in the project management system',
    parameters: {
      type: 'object',
      properties: {
        title: {
          type: 'string',
          description: 'Task title',
        },
        description: {
          type: 'string',
          description: 'Task description',
        },
        priority: {
          type: 'string',
          enum: ['low', 'medium', 'high'],
          description: 'Task priority',
        },
      },
      required: ['title', 'description'],
    },
  },
  {
    name: 'send_email',
    description: 'Send an email to a recipient',
    parameters: {
      type: 'object',
      properties: {
        to: {
          type: 'string',
          description: 'Recipient email address',
        },
        subject: {
          type: 'string',
          description: 'Email subject',
        },
        body: {
          type: 'string',
          description: 'Email body content',
        },
      },
      required: ['to', 'subject', 'body'],
    },
  },
  {
    name: 'query_database',
    description: 'Execute a SQL query on the database',
    parameters: {
      type: 'object',
      properties: {
        query: {
          type: 'string',
          description: 'SQL query to execute',
        },
        params: {
          type: 'array',
          description: 'Query parameters for prepared statements',
        },
      },
      required: ['query'],
    },
  },
];

Implement Tool Execution

Create a Switch node that routes to different tool implementations based on the tool name chosen by the AI:

// In the Switch node
const toolName = $json.tool_calls?.[0]?.function?.name;

// Route to appropriate branch:
// - search_knowledge_base
// - create_task
// - send_email
// - query_database

return toolName;

Step 4: OpenAI Integration with Function Calling

Now we integrate OpenAI with function calling enabled:

Configure OpenAI Node

Add an OpenAI node with these settings:

// Build the messages array
const context = $json.context || {};
const previousMessages = $json.messages || [];
const userMessage = $input.item.json.body.message;

const messages = [
  {
    role: 'system',
    content: `You are an intelligent AI agent with access to various tools.

Your role is to:
1. Understand user goals and break them into actionable steps
2. Use available tools to accomplish tasks
3. Provide clear, concise responses
4. Handle errors gracefully and adapt your approach

Current context:
- Completed tasks: ${context.completed_tasks?.length || 0}
- Pending tasks: ${context.pending_tasks?.length || 0}

Always explain your reasoning and which tools you're using.`,
  },
  ...previousMessages,
  {
    role: 'user',
    content: userMessage,
  },
];

return {
  model: 'gpt-4-turbo-preview',
  messages: messages,
  functions: tools, // Our tool definitions
  function_call: 'auto', // Let AI decide when to use tools
  temperature: 0.7,
  max_tokens: 1000,
};

Step 5: Agent Decision Loop

The core of the agent is a loop that:

  1. Gets AI response
  2. Checks if tools were called
  3. Executes tools
  4. Feeds results back to AI
  5. Repeats until task is complete

Implement the Loop

Use an If node to check if the AI wants to use a tool:

// Check if function was called
const response = $json;
const hasFunctionCall = response.choices?.[0]?.message?.function_call !== undefined;

return hasFunctionCall;

If yes: Execute the tool and feed results back to OpenAI

If no: Return the response to the user

Tool Execution Example: Vector Search

// In the search_knowledge_base tool branch
const toolArgs = JSON.parse($json.choices[0].message.function_call.arguments);
const query = toolArgs.query;
const limit = toolArgs.limit || 5;

// Create embedding for the query
const embeddingResponse = await fetch('https://api.openai.com/v1/embeddings', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    model: 'text-embedding-3-small',
    input: query,
  }),
});

const embedding = await embeddingResponse.json();
const vector = embedding.data[0].embedding;

// Query Pinecone
const pineconeResponse = await fetch(
  `https://${process.env.PINECONE_INDEX}-${process.env.PINECONE_ENVIRONMENT}.svc.pinecone.io/query`,
  {
    method: 'POST',
    headers: {
      'Api-Key': process.env.PINECONE_API_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      vector: vector,
      topK: limit,
      includeMetadata: true,
    }),
  }
);

const results = await pineconeResponse.json();

// Format results for AI
return {
  tool_name: 'search_knowledge_base',
  tool_results: results.matches.map(match => ({
    content: match.metadata.text,
    score: match.score,
    source: match.metadata.source,
  })),
};

Step 6: Memory and Long-Term Context

For production agents, memory is essential. Implement two types:

Short-Term Memory (Conversation History)

Store in Supabase with a rolling window:

// Keep last 20 messages
const maxMessages = 20;
const updatedMessages = [
  ...previousMessages.slice(-maxMessages + 2), // Keep recent history
  {
    role: 'user',
    content: userMessage,
  },
  {
    role: 'assistant',
    content: assistantResponse,
  },
];

// Update in Supabase
await supabase
  .from('agent_conversations')
  .update({
    messages: updatedMessages,
    updated_at: new Date().toISOString(),
  })
  .eq('session_id', sessionId);

Long-Term Memory (Vector Database)

Store important information as embeddings:

// Identify important exchanges to store
const shouldStore =
  userMessage.length > 100 || // Long messages
  /save|remember|important|note/.test(userMessage.toLowerCase()) || // Keywords
  context.metadata.total_interactions % 5 === 0; // Every 5th interaction

if (shouldStore) {
  // Create embedding
  const embedding = await createEmbedding(userMessage + ' ' + assistantResponse);

  // Store in Pinecone
  await pinecone.upsert({
    vectors: [
      {
        id: `${sessionId}-${Date.now()}`,
        values: embedding,
        metadata: {
          session_id: sessionId,
          user_id: userId,
          text: userMessage + ' ' + assistantResponse,
          timestamp: new Date().toISOString(),
        },
      },
    ],
  });
}

Step 7: Error Handling and Recovery

Production agents need robust error handling:

Implement Try-Catch Wrapper

try {
  // Agent execution logic
  const result = await executeAgentStep();
  return result;
} catch (error) {
  // Log error
  console.error('Agent error:', error);

  // Determine if error is recoverable
  if (error.code === 'rate_limit_exceeded') {
    // Wait and retry
    await sleep(2000);
    return { retry: true };
  }

  if (error.code === 'context_length_exceeded') {
    // Summarize conversation and continue
    const summary = await summarizeConversation(messages);
    return {
      action: 'reset_context',
      summary: summary,
    };
  }

  // For unrecoverable errors, inform user gracefully
  return {
    role: 'assistant',
    content: `I encountered an issue: ${error.message}. Let me try a different approach.`,
    error: true,
  };
}

Circuit Breaker Pattern

Prevent infinite loops:

const MAX_ITERATIONS = 10;
let iterations = 0;

while (iterations < MAX_ITERATIONS) {
  iterations++;

  const response = await callOpenAI();

  if (!response.function_call) {
    // Task complete
    break;
  }

  // Execute tool and continue
  const toolResult = await executeTool(response.function_call);

  if (iterations === MAX_ITERATIONS) {
    return {
      role: 'assistant',
      content:
        'This task is taking longer than expected. Would you like me to break it down differently?',
    };
  }
}

Step 8: Monitoring and Observability

Track agent performance and behavior:

// Log each interaction
const logEntry = {
  session_id: sessionId,
  timestamp: new Date().toISOString(),
  user_message: userMessage,
  assistant_response: assistantResponse,
  tools_used: toolsUsed,
  tokens_used: tokensUsed,
  duration_ms: executionTime,
  success: !hasError,
};

// Store in monitoring system
await supabase.from('agent_logs').insert(logEntry);

// Calculate metrics
const metrics = {
  avg_response_time: calculateAverage('duration_ms'),
  success_rate: calculateRate('success'),
  most_used_tools: aggregateToolUsage(),
  token_consumption: sumTokens(),
};

Security Best Practices

When building AI agents, security is paramount:

1. Input Validation

// Sanitize user input
const sanitizeInput = input => {
  // Remove potential injection attempts
  const cleaned = input
    .replace(/<script[^>]*>.*<\/script>/gi, '')
    .replace(/javascript:/gi, '')
    .trim();

  // Limit length
  if (cleaned.length > 5000) {
    throw new Error('Input too long');
  }

  return cleaned;
};

2. Tool Permission System

// Define which tools each user can access
const userPermissions = {
  user_id_123: ['search_knowledge_base', 'create_task'],
  admin_id_456: ['search_knowledge_base', 'create_task', 'query_database', 'send_email'],
};

// Check before tool execution
const canUseTool = (userId, toolName) => {
  const permissions = userPermissions[userId] || [];
  return permissions.includes(toolName);
};

3. Rate Limiting

// Implement per-user rate limits
const rateLimits = new Map();
const MAX_REQUESTS_PER_MINUTE = 20;

const checkRateLimit = userId => {
  const now = Date.now();
  const userRequests = rateLimits.get(userId) || [];

  // Filter requests from last minute
  const recentRequests = userRequests.filter(time => now - time < 60000);

  if (recentRequests.length >= MAX_REQUESTS_PER_MINUTE) {
    throw new Error('Rate limit exceeded');
  }

  recentRequests.push(now);
  rateLimits.set(userId, recentRequests);
};

Advanced Patterns

Multi-Agent Orchestration

For complex tasks, use multiple specialized agents:

// Define specialized agents
const agents = {
  researcher: {
    system_prompt: 'You are a research specialist. Your job is to gather and analyze information.',
    tools: ['search_knowledge_base', 'web_search'],
  },
  writer: {
    system_prompt: 'You are a writing specialist. You create clear, engaging content.',
    tools: ['search_knowledge_base'],
  },
  executor: {
    system_prompt: 'You are an execution specialist. You perform actions and tasks.',
    tools: ['create_task', 'send_email', 'query_database'],
  },
};

// Route to appropriate agent based on task
const determineAgent = async task => {
  const classification = await classifyTask(task);
  return agents[classification];
};

Reflection and Self-Improvement

Enable agents to evaluate their own performance:

// After task completion, ask agent to reflect
const reflection = await callOpenAI({
  messages: [
    {
      role: 'system',
      content: 'Review the conversation and identify what went well and what could be improved.',
    },
    ...conversationHistory,
  ],
});

// Store insights for future improvements
await storeReflection(sessionId, reflection);

Testing Your Agent

Create comprehensive test cases:

const testCases = [
  {
    name: 'Simple information retrieval',
    input: 'What are our Q4 revenue numbers?',
    expectedTools: ['search_knowledge_base'],
    shouldSucceed: true,
  },
  {
    name: 'Multi-step task',
    input: 'Create a summary report of last month and email it to the team',
    expectedTools: ['query_database', 'search_knowledge_base', 'send_email'],
    shouldSucceed: true,
  },
  {
    name: 'Error recovery',
    input: 'Access restricted database',
    expectedTools: ['query_database'],
    shouldSucceed: false,
    expectedError: 'permission_denied',
  },
];

// Run tests
for (const test of testCases) {
  const result = await runAgentTest(test);
  assert(result.success === test.shouldSucceed);
  assert(result.toolsUsed.every(tool => test.expectedTools.includes(tool)));
}

Performance Optimization

Caching Strategy

// Cache common queries
const cache = new Map();
const CACHE_TTL = 300000; // 5 minutes

const getCached = key => {
  const cached = cache.get(key);
  if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
    return cached.value;
  }
  return null;
};

const setCached = (key, value) => {
  cache.set(key, {
    value,
    timestamp: Date.now(),
  });
};

Parallel Tool Execution

When tools don't depend on each other:

// Execute independent tools in parallel
const toolCalls = response.tool_calls || [];
const independentTools = identifyIndependentTools(toolCalls);

const results = await Promise.all(independentTools.map(tool => executeTool(tool)));

Production Deployment Checklist

  • Environment variables configured securely
  • Rate limiting implemented
  • Error handling and logging in place
  • Monitoring and alerting set up
  • User permission system configured
  • Input validation and sanitization active
  • Backup and recovery procedures documented
  • Cost limits and budgets set in OpenAI dashboard
  • Load testing completed
  • Documentation updated

Next Steps

Now that you have a production-ready AI agent, consider:

  1. Expand tool capabilities - Add integrations with Supabase, Strapi, and other tools
  2. Implement multi-modal capabilities - Add vision and audio processing
  3. Build specialized agents - Create domain-specific agents for different use cases
  4. Add human-in-the-loop - Implement approval workflows for sensitive operations
  5. Scale horizontally - Deploy multiple agent instances with load balancing

Join the Community

Building AI agents is an evolving field, and the House of Loops community is at the forefront. Join us to:

  • Share your agent implementations
  • Get help troubleshooting complex scenarios
  • Access exclusive agent templates and workflows
  • Participate in live workshops on advanced AI topics
  • Connect with other developers building production AI systems

Join House of Loops Today and get $100K+ in startup credits including OpenAI credits to power your AI agents.


Have questions about implementing AI agents? Drop by our community and let's discuss your use case!

H

House of Loops Team

House of Loops is a technology-focused community for learning and implementing advanced automation workflows using n8n, Strapi, AI/LLM, and DevSecOps tools.

Join Our Community

Join 1,000+ automation enthusiasts