Travel Planner — n8n + Dify Integration Guide
My TravelPlanner — n8n + Dify Integration Guide
Version: 1.0
Date: 2026-03-30
Scope: n8n workflow orchestration and Dify knowledge base management for the MTP development team
See also: ITI/operations/documentation/GUIDE-TO-ALL-AGENTS-AND-SKILLS.md (full ITI-wide catalog) and ITI/operations/documentation/QUICKSTART-GUIDE.md (ITI-wide procedures)
1. Architecture Overview
The Two Platforms
My TravelPlanner is deployed on two platforms that share the same n8n + Dify backend:
| Platform | Entry point | Adapter |
|---|---|---|
| WordPress plugin (PHP) | MTP_Orchestrator::chat() |
ITI_Workflow_Adapter |
| iOS/macOS app (Swift) | MTPOrchestrator.process() |
N8nWorkflowClient |
Both platforms implement the same dispatch pattern: try n8n first, fall back to local agents if n8n is unavailable.
Dispatch Flow
User message
│
▼
┌────────────────────────────────────────────────────────┐
│ Platform Layer │
│ WordPress: MTP_Orchestrator::chat() │
│ iOS/macOS: MTPOrchestrator.process() │
└──────────────────────┬─────────────────────────────────┘
│ health check → GET /healthz (2s timeout)
▼
n8n available?
/ \
YES NO
│ │
▼ ▼
┌───────────────────┐ ┌─────────────────────────────────┐
│ POST /webhook/ │ │ Local Fallback │
│ travelplanner │ │ PHP: keyword routing → 17 │
│ (120s timeout) │ │ specialist agents │
│ │ │ → ITI_Claude_API │
│ n8n AI Agent: │ │ Swift: confidence scoring → │
│ • Claude model │ │ agent selection → RAG │
│ • Simple Memory │ │ pipeline → direct Claude │
│ • Dify KB tool │ └─────────────────────────────────┘
└────────┬──────────┘
│
▼
JSON response
{output: "..."}
Fallback Differences Between Platforms
- PHP (WordPress):
ITI_Workflow_Adapterhas an in-adapter Claude fallback. When the constructor receives a$claude_apiinstance (MTP always passes one), any n8n failure automatically retries via direct Claude. The orchestrator only sees aWP_Errorif both n8n and Claude fail. - Swift (iOS/macOS):
N8nWorkflowClientdoes not embed a Claude fallback. The orchestrator catches any thrown error fromrunAgent()and falls through to the full local RAG pipeline (KB files + Pinecone + Tavily + direct Claude).
The 9-Container Docker Stack
MTP touches four of the nine containers:
| Container | Port | MTP’s use |
|---|---|---|
iti-n8n |
5678 | Hosts the travelplanner webhook workflow |
iti-dify-api |
(internal) | Knowledge base retrieval API |
iti-dify-nginx |
3001 | Reverse proxy for Dify API; n8n calls this |
iti-postgres |
5432 | Shared DB for n8n state + Dify KB + pgvector |
Redis (iti-redis) is used by Dify internally for async tasks but MTP does not call it directly.
Stack location: ITI/infrastructure/n8n-dify/docker-compose.yml
2. How MTP Uses n8n Today
WordPress Path
Source: my-travelplanner/includes/class-mtp-orchestrator.php
The chat() method is the entry point for all WordPress chatbot requests. The n8n branch runs at the very top — before keyword routing, KB edit commands, or any specialist agent:
public function chat( string $user_message, string $session_id, array $context = [] ): array {
// Route through n8n when available (orchestration + KB retrieval built in).
if ( class_exists( 'ITI_Workflow_Adapter' ) ) {
$adapter = new ITI_Workflow_Adapter( $this->claude_api );
$n8n_result = $adapter->run_agent( 'travelplanner', $user_message, $session_id );
if ( ! is_wp_error( $n8n_result ) ) {
$output = ITI_Workflow_Adapter::extract_output( $n8n_result );
return [
'success' => true,
'data' => [ 'message' => $output, 'agent' => 'n8n' ],
'message' => $output,
'meta' => [ 'source' => 'n8n' ],
];
}
}
// ... keyword routing to specialist agents follows
}
Key points:
- The adapter is instantiated with
$this->claude_api— this enables the in-adapter Claude fallback extract_output()normalizes n8n’s JSON response, handlingoutput,text, and Anthropic-stylecontent[]blocks- When n8n succeeds, the response metadata shows
source: n8nin logs - When n8n is down, execution falls through to
route()→ keyword-matched specialist agents
iOS/macOS Path
Source: MyTravelPlanner/Services/Agents/MTPOrchestrator.swift
The Swift process() method checks n8n availability after country/city detection:
func process(query: String, context: MTPAgentContext) async -> MTPAgentResult {
// ... detect country/city first ...
// Try n8n workflow first (routing + KB retrieval built in).
if await N8nWorkflowClient.shared.isAvailable() {
do {
let text = try await N8nWorkflowClient.shared.runAgent(
webhookPath: "travelplanner",
query: query,
sessionId: ctx.sessionID
)
return MTPAgentResult(success: true, response: text, agentID: "n8n", ...)
} catch { /* fall through to local routing */ }
}
// Help system, KB edit, agent selection, RAG pipeline, direct Claude follow...
}
Source: MyTravelPlanner/Services/N8nWorkflowClient.swift
The Swift client implements the same contract as the PHP adapter:
func isAvailable() async -> Bool {
// GET http://localhost:5678/healthz (2s timeout)
}
func runAgent(webhookPath: String, query: String, sessionId: String? = nil,
timeout: TimeInterval = 120) async throws -> String {
// POST http://localhost:5678/webhook/{webhookPath}
// Body: {"query": "...", "sessionId": "..."}
// Decodes N8nResponse: prefers "output" field, then "text" field
}
What the n8n Workflow Receives
Every call to the travelplanner webhook sends:
{
"query": "user message text",
"sessionId": "browser-session-or-app-session-id"
}
The sessionId is used by the Simple Memory node inside n8n to maintain conversation continuity across turns.
What the n8n Workflow Returns
The expected response shape (extracted by extract_output() / N8nResponse):
{ "output": "AI response text here" }
The adapter also handles {"text": "..."} and Anthropic-style {"content": [{"type": "text", "text": "..."}]} shapes.
3. How MTP Uses Dify Today
MTP’s Knowledge Base in Dify
MTP’s full knowledgebase directory (products/my-travelplanner.com/knowledgebase/) is indexed as a Dify dataset with pgvector semantic search. The KB contains 183+ markdown files organized as:
knowledgebase/
├── countries/ Country overviews + city guides (18 countries)
│ ├── france/overview.md, cities/paris.md, cities/nice.md
│ ├── thailand/overview.md, cities/bangkok.md
│ └── ... (18 countries, 50+ city files)
├── destinations/ Regional guides (Asia, Europe, South America)
├── scuba/ Dive travel, certification, regional dive sites
├── travel/ Civil unrest guide, general travel
├── travel-guides/ Restaurant guides, transit guides for major cities
├── accommodation/ Lodging types, booking strategies
├── remote-work/ Digital nomad resources
├── airports/ Airport guides
├── rv-camping/ RV travel
├── expat/ Expat relocation
└── fifa-2026/ FIFA 2026 World Cup (25 files)
├── overview.md, schedule.md, venues.md, teams.md
├── us-cities/ (11 host cities)
├── mexico-cities/ (3 host cities)
└── canada-cities/ (2 host cities)
How n8n Queries Dify
Inside the n8n TravelPlanner Router workflow, each specialist AI Agent node has an attached HTTP Request Tool configured as:
| Field | Value |
|---|---|
| URL | http://dify-nginx/v1/datasets/{mtp_dataset_id}/retrieve |
| Method | POST |
| Authorization | Bearer {dataset-scoped API token} |
| Body | See below |
Request body (n8n expression):
={{
{
"query": ($json.chatInput || "general travel query"),
"retrieval_model": {
"search_method": "semantic_search",
"reranking_enable": false,
"top_k": 3,
"score_threshold_enabled": false
}
}
}}
Note: use || (not ??) for the fallback — n8n expressions handle empty strings differently than JavaScript.
iOS App Fallback Uses Bundle KB + Pinecone
When the iOS app falls back to local agents (n8n unavailable), it uses:
- Bundle-included KB files — markdown files bundled with the app via
Bundle.main.url(forResource:) - Pinecone — vector search as a secondary enrichment source
- Tavily — web search for current information
These are the pre-n8n paths that remain active as the fallback layer.
4. Creating a New Agent Workflow for MTP
Use this process to build or rebuild the MTP n8n workflow from Cursor using the mcp-n8n SDK.
Step 1: Start the Stack
cd ITI/infrastructure/n8n-dify
docker compose up -d
docker compose ps # verify all 9 containers healthy
Verify n8n is running: http://localhost:5678
Step 2: Read the SDK Reference
In Cursor, invoke the n8n workflow development skill:
Using the skill at ITI/operations/Skills/Claude-Skills/n8n-workflow-development/SKILL.md,
help me design a Router workflow for My TravelPlanner that routes to specialist agents
via a Switch node and connects each agent to the MTP Dify knowledge base.
Or use the MCP tool directly to read the SDK reference resource.
Step 3: Discover Nodes
Use the search_nodes MCP tool from Cursor:
search_nodes(["AI Agent", "Anthropic Chat Model", "Simple Memory", "HTTP Request Tool", "Switch", "Webhook", "Respond to Webhook"])
Note the node IDs and discriminators returned — you need these for get_node_types.
Step 4: Get Type Definitions
get_node_types([
{ nodeId: "@n8n/n8n-nodes-langchain.agent", resource: "...", operation: "..." },
{ nodeId: "@n8n/n8n-nodes-langchain.lmChatAnthropic" },
{ nodeId: "@n8n/n8n-nodes-langchain.memoryBufferWindow" },
{ nodeId: "n8n-nodes-base.httpRequestTool" },
{ nodeId: "n8n-nodes-base.switch" },
{ nodeId: "n8n-nodes-base.webhook" },
{ nodeId: "n8n-nodes-base.respondToWebhook" }
])
Do not skip this step — guessing parameter names creates invalid workflows.
Step 5: Write Workflow Code (Router Pattern)
The MTP workflow uses the Router pattern: a classifier AI Agent reads the query and outputs a category string, which a Switch node uses to route to the correct specialist agent.
import { workflow, node, trigger, switchCase, languageModel, memory, tool } from '@n8n/workflow-sdk';
// Webhook trigger — receives {"query": "...", "sessionId": "..."}
const webhookTrigger = trigger({ type: 'n8n-nodes-base.webhook', ... });
// Classifier agent — outputs one of: travel, scuba, culinary, rv, expat, ...
const classifier = node({
type: '@n8n/n8n-nodes-langchain.agent',
config: {
name: 'MTP Classifier',
parameters: {
systemMessage: `You are a routing classifier for My Travel Planner.
Classify the user query into exactly one category:
- scuba: dive, diving, scuba, reef, liveaboard, snorkel
- rv: rv, motorhome, camper van, boondocking, campground
- expat: expat, relocate, visa, cost of living, retire abroad
- culinary: food tour, restaurant, michelin, street food, wine
- weather: weather, climate, best time to visit, rainy season
- hotel: hotel, resort, hostel, accommodation, where to stay
- planning: itinerary, day by day, how many days, trip plan
- fifa: world cup, fifa, fan zone, stadium, match schedule
- travel: (default — everything else)
Respond with ONLY the category name. No explanation.`,
}
},
output: [{ output: 'travel' }] // example output shape
});
// Switch node routes on classifier output
const router = switchCase({
config: {
parameters: {
dataType: 'string',
value1: '={{ $json.output }}',
rules: { values: [
{ value: 'scuba' },
{ value: 'rv' },
// ... one per specialist ...
{ value: 'travel' }, // default
]}
}
}
});
// Each specialist agent connects to the Dify KB tool
const travelAgent = node({ type: '@n8n/n8n-nodes-langchain.agent', config: {
name: 'Travel Advisor',
parameters: {
systemMessage: 'You are an expert travel advisor...',
}
}});
// ... define scubaAgent, rvAgent, etc. ...
// Compose
export default workflow('mtp-travelplanner', 'My TravelPlanner Router')
.add(webhookTrigger)
.to(classifier)
.to(router)
// router branches to each specialist
.add(router.output(0).to(scubaAgent))
.add(router.output(1).to(rvAgent))
// ...
.add(router.output(8).to(travelAgent));
Critical expression rules:
// CORRECT — use || for fallbacks in expressions
={{ $json.chatInput || "general travel query" }}
// WRONG — ?? does not treat empty strings as falsy in n8n
={{ $json.chatInput ?? "general travel query" }}
// CORRECT — memory session key (survives routing nodes)
={{ $execution.id }}
// WRONG — lost after Switch/Merge nodes
={{ $json.body.sessionId }}
Step 6: Validate
validate_workflow(code)
Fix all reported errors before proceeding. Common issues: wrong parameter names, missing required fields, version mismatches.
Step 7: Deploy and Publish
create_workflow_from_code(code, "MTP TravelPlanner Router — routes to 9 specialist agents with Dify KB")
Then publish — unpublished workflows do not serve production webhooks:
publish_workflow(workflowId)
Step 8: Test
# Basic smoke test
curl -s -X POST http://localhost:5678/webhook/travelplanner \
-H "Content-Type: application/json" \
-d '{"query": "best diving spots in Thailand"}' | python3 -m json.tool
# Test session continuity
curl -s -X POST http://localhost:5678/webhook/travelplanner \
-H "Content-Type: application/json" \
-d '{"query": "what is the weather like there?", "sessionId": "test-session-1"}' | python3 -m json.tool
# Test FIFA routing
curl -s -X POST http://localhost:5678/webhook/travelplanner \
-H "Content-Type: application/json" \
-d '{"query": "where is the World Cup final in 2026?"}' | python3 -m json.tool
Expected: HTTP 200, JSON body with output or response key containing text.
5. Creating a New Skill Pipeline for MTP
Agent vs. Skill — The MTP Distinction
| Concept | What it is | Where it lives |
|---|---|---|
| Agent | An AI persona with domain expertise, KB context, and routing | n8n workflow (AI Agent node) or PHP/Swift class |
| Skill | Reusable methodology invoked in a prompt — no persona | ITI/operations/Skills/Claude-Skills/{skill-name}/SKILL.md |
| Workflow | n8n automation that orchestrates one or more agents + tools | n8n (webhook → nodes → response) |
Skills are not n8n nodes. A skill is a SKILL.md file that you reference in an agent’s system prompt or in a Cursor session to apply specialized methodology. Skills guide how an agent reasons, not what infrastructure it calls.
When to Create a Skill for MTP
Create a skill when you need MTP agents to apply a repeatable methodology that:
- Should work across multiple advisors (e.g., budget estimation logic used by both the Planning Assistant and the RV Advisor)
- Should be testable and auditable independently
- Could be useful in other ITI products
Examples of good MTP skill candidates:
trip-budget-estimation— standardized cost breakdown methodologyitinerary-optimization— day-by-day sequencing heuristicsdestination-safety-assessment— structured safety evaluation for civil unrest or weather
Step-by-Step: New MTP Skill
1. Check for existing coverage
grep -i "budget\|itinerary\|safety" ITI/operations/Skills/SKILLS-INDEX.md
2. Create the SKILL.md
File: ITI/operations/Skills/Claude-Skills/{skill-name}/SKILL.md
---
name: Trip Budget Estimation
description: Standardized methodology for estimating travel budgets across accommodation, transport, food, and activities.
---
## Instructions
When estimating a travel budget, apply this structured breakdown:
1. **Accommodation**: [methodology...]
2. **Transport**: [methodology...]
3. **Food**: [methodology...]
4. **Activities**: [methodology...]
5. **Buffer**: Add 15% contingency
...
3. Wire the skill into the MTP n8n workflow
Reference the skill in your AI Agent node’s system prompt:
You are the MTP Planning Assistant advisor.
When estimating costs for trips, apply the methodology defined in the
ITI trip-budget-estimation skill:
[paste the skill instructions here, or use a Dify KB document that contains the skill]
The cleanest approach: upload the SKILL.md to the MTP Dify KB so the AI Agent retrieves it via semantic search when budget-related queries come in. This keeps the system prompt lean and makes the skill updatable without redeploying the workflow.
4. Register in global indexes
# Add to Skills index
# Edit: ITI/operations/Skills/SKILLS-INDEX.md
# Edit: ITI/operations/agents-and-skills.md (Skills section)
# Create: ITI/operations/Skills/{skill-id}.json
# Sync to Cursor skills
cp -r ITI/operations/Skills/Claude-Skills/{skill-name} ~/.cursor/skills/
5. Add changelog entry
# In ITI/operations/agents-skills-changelog.md (top of file)
## vX.Y — 2026-MM-DD
### Added
- `trip-budget-estimation`: Standardized travel budget methodology for MTP Planning Assistant
6. Adding a New Specialist Agent to the MTP Router Workflow
Use this process when you need to add a new specialist (e.g., a “Photography Advisor”) to the existing TravelPlanner Router workflow.
Step 1: Update Both Platforms in Parallel
A new MTP specialist requires changes in three places:
- n8n workflow — new AI Agent node + Switch case + classifier prompt update
- PHP orchestrator — new keyword constant +
register_agent()call + fallback class - Swift orchestrator — new agent struct +
registerDefaultAgents()+canHandle()scoring
Step 2: Update the n8n Workflow
In the n8n UI (http://localhost:5678):
- Open the TravelPlanner Router workflow
- Add a new AI Agent node:
- Name:
Photography Advisor - Language Model: Anthropic Claude (same credential as other agents)
- Memory: Simple Memory with
={{ $execution.id }}as session key - System prompt: travel photography persona + domain expertise
- Add an HTTP Request Tool to the new agent node (for Dify KB):
- URL:
http://dify-nginx/v1/datasets/{mtp_dataset_id}/retrieve - Authorization: Bearer
{MTP dataset token} - Body:
={{ {"query": ($json.chatInput || ""), "retrieval_model": {"search_method": "semantic_search", "top_k": 3}} }}
- Add a new case to the Switch node:
- Value:
photography - Connect output to the Photography Advisor node
- Update the classifier agent system prompt to include:
- photography: camera settings, golden hour, composition, photo spots, Instagram
- Save and re-publish the workflow
Via mcp-n8n SDK (preferred for reproducibility):
get_workflow_details(workflowId) # inspect current workflow structure
# ... modify code to add new agent node and switch case ...
validate_workflow(updatedCode)
update_workflow(workflowId, updatedCode)
publish_workflow(workflowId)
Step 3: Update the PHP Orchestrator
In class-mtp-orchestrator.php:
// Add keyword constant
const PHOTOGRAPHY_KEYWORDS = [
'photography', 'photo spot', 'golden hour', 'instagram', 'camera',
'shoot', 'composition', 'landscape photography', 'street photography',
];
// In __construct(), after other conditional agent registrations:
if ( class_exists( 'MTP_Photography_Agent' ) ) {
$this->photography_agent = new MTP_Photography_Agent( $this->claude_api );
$this->register_agent( 'photography_advisor', $this->photography_agent );
}
// In route(), add to routing table (before the default travel_advisor):
[ self::PHOTOGRAPHY_KEYWORDS, 'photography_advisor' ],
Create class-mtp-photography-agent.php:
class MTP_Photography_Agent extends ITI_Base_Agent {
public function __construct( ITI_Claude_API $claude_api ) {
$this->name = 'photography_advisor';
$this->display_name = 'Photography Advisor';
parent::__construct( $claude_api );
}
public function get_system_prompt(): string {
return "You are an expert travel photography advisor...";
}
public function run( $user_message, $context = [] ): array {
$kb_content = $this->load_kb_section( $user_message );
return $this->call_claude( $user_message, $context, $kb_content );
}
}
Step 4: Update the Swift App
In MTPOrchestrator.swift, add to registerDefaultAgents():
PhotographyAdvisorAgent(),
Create PhotographyAdvisorAgent.swift:
struct PhotographyAdvisorAgent: MTPAgent {
let id = "photography_advisor"
let name = "Photography Advisor"
let fallbackAgentID: String? = "travel_advisor"
func canHandle(query: String, context: MTPAgentContext) -> Double {
let lower = query.lowercased()
let keywords = ["photography", "photo spot", "golden hour", "instagram", "camera"]
let matches = keywords.filter { lower.contains($0) }.count
return Double(matches) * 0.4
}
func systemPrompt(context: MTPAgentContext) -> String {
return "You are an expert travel photography advisor..."
}
func kbPaths() -> [(keywords: [String], filename: String)] {
return [
(["photography", "photo"], "knowledgebase/travel/photography-guide"),
]
}
}
Step 5: Test All Paths
# Test n8n workflow path
curl -s -X POST http://localhost:5678/webhook/travelplanner \
-H "Content-Type: application/json" \
-d '{"query": "best photo spots for golden hour in Santorini"}' | python3 -m json.tool
# Test WordPress fallback (with n8n stopped)
docker stop iti-n8n
# Make request via WordPress chatbot — should route to photography_advisor
docker start iti-n8n
Step 6: Register in Global Indexes
# 1. Create agent definition files
# ITI/operations/Agents/products/mtp-photography-advisor-agent.md
# ITI/operations/Agents/products/mtp-photography-advisor-agent.json
# 2. Update AGENTS-INDEX.json (increment total_agents)
# 3. Update agents-and-skills.md — add row to MTP agents table (Section 3.1)
# 4. Update GUIDE-TO-ALL-AGENTS-AND-SKILLS.md — add to Section 3.1 table
# 5. Update CLAUDE.md — add new agent to AI Agent Inventory table
# 6. Add changelog entry to agents-skills-changelog.md
7. Managing MTP’s Dify Knowledge Base
Accessing the Dify Console
With the Docker stack running, open: http://localhost:3000
Navigate to Knowledge to see all datasets. The MTP dataset is named “My TravelPlanner KB” (or similar — confirm in the UI).
Adding New Content
Single file upload (preferred for small additions):
- Open the MTP dataset in the Dify console
- Click Add File
- Drag and drop your markdown file
- Select indexing mode: High Quality (uses
text-embedding-3-small) - Leave chunking on Automatic
- Click Save and Process — Dify re-indexes automatically
Supported formats: .md, .txt, .pdf, .docx, .csv, .html, .json, .xml
Batch upload via script:
cd ITI/infrastructure/n8n-dify
./kb-migrate.sh travelplanner # re-upload all MTP KB files
The kb-migrate.sh script authenticates into the Dify API, creates/updates the dataset, and uploads files from products/my-travelplanner.com/knowledgebase/. Check the script for current dataset IDs and credentials.
Testing Retrieval Quality
After uploading content, test retrieval before relying on it in production:
- Dify console → Knowledge → select MTP dataset → Retrieval Testing tab
- Enter test queries matching your new content:
"best dive sites in the Maldives"— should return scuba files"FIFA 2026 stadium capacity"— should return FIFA venue files"digital nomad visa Portugal"— should return expat/Portugal files
- Review the returned chunks and their similarity scores
- If quality is poor, try re-indexing: Settings → Re-index (or delete and re-upload with cleaner formatting)
Re-indexing After Bulk Updates
When you update many files or change the embedding model, trigger a re-index:
# Check current dataset state in PostgreSQL
docker compose -f ITI/infrastructure/n8n-dify/docker-compose.yml \
exec -T postgres psql -U postgres -d dify \
-c "SELECT id, name, document_count FROM datasets ORDER BY created_at;"
# Via Dify API (requires dataset token from .env or DB)
curl -X POST http://localhost:3001/v1/datasets/{dataset_id}/indexing-estimate \
-H "Authorization: Bearer {dataset_token}" \
-H "Content-Type: application/json"
Re-indexing is safe and non-destructive — queries continue using the old index until re-indexing completes.
MTP KB Directory to Dify Dataset Mapping
| KB directory | Dify content | Primary agents that use it |
|---|---|---|
knowledgebase/countries/ |
Country + city overviews | Travel Advisor, Local Guide, Planning Assistant |
knowledgebase/destinations/ |
Regional destination guides | Travel Advisor |
knowledgebase/scuba/ |
Dive sites, certification, travel tips | Scuba Advisor |
knowledgebase/fifa-2026/ |
World Cup venues, schedules, cities | FIFA 2026 Advisor |
knowledgebase/expat/ |
Relocation, visa, cost of living | Expat Advisor, Retirement Advisor |
knowledgebase/rv-camping/ |
RV parks, routes, boondocking | RV Advisor |
knowledgebase/remote-work/ |
Coworking, nomad visas, coliving | Remote Work Advisor |
knowledgebase/accommodation/ |
Hotels, hostels, booking strategies | Hotel Advisor, Rental Advisor |
knowledgebase/airports/ |
Airport guides, terminals, lounges | Airport Advisor |
knowledgebase/travel/ |
Safety, civil unrest | Civil Unrest Advisor |
knowledgebase/travel-guides/ |
Restaurant + transit guides for major cities | Culinary Advisor, Transit Advisor |
All of this content is indexed in one MTP dataset in Dify. The AI Agent nodes retrieve relevant chunks using semantic search — the query itself determines what content is retrieved.
Keeping the iOS Bundle KB in Sync
The iOS app bundles KB files directly (fallback path). When you update content in Dify, also update the corresponding files in knowledgebase/ and rebuild the app to keep the fallback path current.
8. Debugging and Troubleshooting
n8n Execution Logs
Via UI:
- Open http://localhost:5678
- Navigate to Executions (left sidebar)
- Click any execution to inspect inputs, outputs, and errors per node
- Failed nodes are highlighted in red — click to see the error message and stack trace
Via API:
N8N_API_KEY="$(grep N8N_API_KEY ITI/infrastructure/n8n-dify/.env | cut -d= -f2)"
# Recent executions for travelplanner workflow
curl -s "http://localhost:5678/api/v1/executions?limit=10" \
-H "X-N8N-API-KEY: $N8N_API_KEY" | python3 -m json.tool
# Inspect a specific execution with full data
curl -s "http://localhost:5678/api/v1/executions/{executionId}?includeData=true" \
-H "X-N8N-API-KEY: $N8N_API_KEY" | python3 -m json.tool
Common Failures
Webhook returns 404 — {"code": 404, "message": "The requested webhook ... is not registered."}
Cause: workflow is not published (active). Fix:
publish_workflow(workflowId)
Or toggle the Active switch in the n8n UI. Remember: every update_workflow call deactivates the workflow — always re-publish after updates.
Webhook returns 500 or times out
Causes and checks:
# Check n8n container health
docker compose -f ITI/infrastructure/n8n-dify/docker-compose.yml logs --tail=50 n8n
# Check if Dify is reachable from inside n8n
docker compose -f ITI/infrastructure/n8n-dify/docker-compose.yml \
exec iti-n8n curl -s http://dify-nginx/console/api/setup | head -c 100
Expression error: ExpressionExtensionError or undefined
Common cause: using ?? instead of || in n8n expressions.
// BROKEN — ?? treats empty string as valid (returns "")
={{ $json.chatInput ?? "default" }}
// FIXED — || treats empty string as falsy (returns "default")
={{ $json.chatInput || "default" }}
Also common: referencing $json.body.sessionId after a routing node — the body context is lost after Switch. Use $execution.id instead.
Memory not persisting across turns
Cause: session key expression evaluating to a different value each turn. Fix: ensure the Simple Memory node’s session key is ={{ $execution.id }} (not $json.sessionId).
Dify retrieval returns empty results
Steps:
- Open Dify console → Knowledge → MTP dataset → Retrieval Testing
- Test with 2-3 different queries
- If all return empty: check dataset indexing status (may still be processing)
- If some return empty: the content may not be in the KB — upload missing files
- If quality is poor: delete and re-upload with High Quality indexing (not Economy)
Fallback not triggering (PHP)
Verify the adapter is getting the Claude API instance:
// In class-mtp-orchestrator.php, this line MUST pass $this->claude_api
$adapter = new ITI_Workflow_Adapter( $this->claude_api );
// NOT: new ITI_Workflow_Adapter() ← no fallback!
Fallback not triggering (Swift)
The Swift N8nWorkflowClient throws on failure. Ensure the catch block in MTPOrchestrator.process() does not rethrow — the empty catch { } is intentional and means “fall through to local path.”
Verifying Fallback End-to-End
# 1. Stop n8n to force fallback
docker stop iti-n8n
# 2. Make a test request through WordPress
curl -s -X POST https://your-wp-site.com/wp-json/mtp/v1/chat \
-H "Content-Type: application/json" \
-d '{"message": "What are the best beaches in Thailand?", "session_id": "test-fallback"}'
# Response meta.source should be an agent slug (travel_advisor, scuba_advisor, etc.)
# NOT "n8n"
# 3. Restore n8n
docker start iti-n8n
# 4. Run smoke tests
cd ITI/infrastructure/n8n-dify
pytest tests/test_n8n_webhooks.py -m smoke -v
Running the MTP Test Suite
cd ITI/products/my-travelplanner.com/tests
source .venv/bin/activate
# Suite 1: Plugin activation
pytest wordpress/test_suite1_activation.py -v
# Suite 2: REST API endpoints
pytest wordpress/test_suite2_rest_api.py -v
# Suite 3: AI pipeline (requires live WordPress + n8n)
pytest wordpress/test_suite3_ai_pipeline.py -v
# All suites
pytest wordpress/ -v
Infrastructure Health Check
# n8n health
curl -s http://localhost:5678/healthz
# Full MTP infrastructure health (n8n webhook)
curl -s http://localhost:5678/webhook/infra-health | python3 -m json.tool
# All containers running
docker compose -f ITI/infrastructure/n8n-dify/docker-compose.yml ps
9. Quick Reference
Key File Paths
| File | Purpose |
|---|---|
ITI/shared/wordpress/api-clients/class-iti-workflow-adapter.php |
PHP adapter — health check, webhook call, Claude fallback |
products/my-travelplanner.com/my-travelplanner/includes/class-mtp-orchestrator.php |
WordPress orchestrator — n8n dispatch + keyword routing |
products/my-travelplanner.com/MyTravelPlanner/MyTravelPlanner/Services/Agents/MTPOrchestrator.swift |
iOS/macOS orchestrator — n8n dispatch + RAG pipeline |
products/my-travelplanner.com/MyTravelPlanner/MyTravelPlanner/Services/N8nWorkflowClient.swift |
iOS/macOS n8n HTTP client |
ITI/infrastructure/n8n-dify/docker-compose.yml |
9-container Docker stack definition |
ITI/infrastructure/n8n-dify/kb-migrate.sh |
Batch KB upload to Dify |
ITI/infrastructure/n8n-dify/backup.sh |
PostgreSQL + volume backup |
ITI/infrastructure/n8n-dify/tests/ |
Infrastructure pytest suite |
products/my-travelplanner.com/tests/ |
MTP product pytest suite |
ITI/operations/documentation/GUIDE-TO-ALL-AGENTS-AND-SKILLS.md |
Full ITI agent + workflow catalog |
ITI/operations/documentation/QUICKSTART-GUIDE.md |
ITI-wide dev procedures |
MTP Webhook Endpoints
| Path | Pattern | Description |
|---|---|---|
/webhook/travelplanner |
Router | All MTP queries — routes to 17 specialist agents |
Service URLs
| Service | URL | Purpose |
|---|---|---|
| n8n | http://localhost:5678 | Workflow editor + API |
| Dify console | http://localhost:3000 | Knowledge base management |
| Dify API (via nginx) | http://localhost:3001 | Retrieval API, console API |
| Infrastructure health | http://localhost:5678/webhook/infra-health | Stack health JSON |
Useful curl Commands
# Test travelplanner webhook
curl -s -X POST http://localhost:5678/webhook/travelplanner \
-H "Content-Type: application/json" \
-d '{"query": "best diving in the Maldives", "sessionId": "test-1"}' | python3 -m json.tool
# Test FIFA routing
curl -s -X POST http://localhost:5678/webhook/travelplanner \
-H "Content-Type: application/json" \
-d '{"query": "World Cup 2026 final location"}' | python3 -m json.tool
# n8n health check
curl -s http://localhost:5678/healthz
# Infrastructure health (requires running infra-health workflow)
curl -s http://localhost:5678/webhook/infra-health | python3 -m json.tool
# List all n8n workflows
curl -s http://localhost:5678/api/v1/workflows \
-H "X-N8N-API-KEY: $N8N_API_KEY" | python3 -m json.tool
# List Dify datasets
docker compose -f ITI/infrastructure/n8n-dify/docker-compose.yml \
exec -T postgres psql -U postgres -d dify \
-c "SELECT id, name, document_count FROM datasets ORDER BY created_at;"
MTP Development Skill Invocations
# Build or rebuild the MTP n8n workflow
"Using ITI/operations/Skills/Claude-Skills/n8n-workflow-development/SKILL.md,
build a Router workflow for My TravelPlanner with specialist agents for travel,
scuba, rv, expat, culinary, weather, hotel, planning, transit, airport,
navigation, rental, civil unrest, FIFA 2026, and remote work."
# Debug a workflow expression error
"Using ITI/operations/Skills/Claude-Skills/n8n-debugging/SKILL.md,
help me debug this n8n expression error in the MTP travelplanner workflow: [error]"
# Run the infrastructure test suite
"Using ITI/operations/Skills/Claude-Skills/n8n-dify-testing/SKILL.md,
run the smoke tests for the n8n/Dify stack and interpret the results."
# Add a new Dify knowledge base document
"Using ITI/operations/Skills/Claude-Skills/dify-knowledge-base-management/SKILL.md,
help me upload and index this new destination guide to the MTP Dify dataset."
# Audit before building
"Using ITI/operations/Skills/Claude-Skills/prompt-auditor/SKILL.md,
audit these requirements for the new Photography Advisor agent: [requirements]"
Adapter Behavior Reference
| Condition | PHP (ITI_Workflow_Adapter) |
Swift (N8nWorkflowClient) |
|---|---|---|
| n8n healthy + response OK | Returns extracted text | Returns extracted text |
| n8n healthy + HTTP error | Falls back to direct Claude* | Throws, caller falls through to RAG |
| n8n unhealthy (healthz fails) | Falls back to direct Claude* | Returns false from isAvailable(), skips entirely |
| Claude also fails | Returns WP_Error('no_fallback') |
Swift RAG pipeline handles independently |
*Only when ITI_Workflow_Adapter was constructed with a $claude_api instance (MTP always does this).
Maintained by: MTP Development Team
Last Updated: 2026-03-30
Related docs: GUIDE-TO-ALL-AGENTS-AND-SKILLS.md · QUICKSTART-GUIDE.md · CLAUDE.md
