This workflow corresponds to n8n.io template #17359 — we link there as the canonical source.
This workflow follows the Agent → HTTP Request recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"id": "32Xb2RqLEyzio5yH",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Dynamic Tour Package Repricing Engine",
"tags": [],
"nodes": [
{
"id": "450119d4-4934-45af-8363-906bc171092b",
"name": "Sticky Note - Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
0,
0
],
"parameters": {
"width": 1020,
"height": 1176,
"content": "## Dynamic Tour Package Repricing Engine\n\nAirline-style yield management for tour packages. This workflow continuously pulls live booking/demand signals and competitor pricing, scores each package's demand momentum and seasonality, checks price positioning against the market, and lets AI finalize a margin-protecting price - live, without a revenue manager repricing by hand.\n\n### Who's it for\n- Tour operators and OTAs running multiple departures per package across a season\n- Revenue management teams that want airline-style dynamic pricing without building it from scratch\n- Anyone whose current process is a person periodically eyeballing a spreadsheet of bookings and competitor prices\n\n### How it works\n1. A schedule trigger polls a live booking/demand API and a competitor pricing API for every active package\n2. JS - Calculate Demand Score, Seasonality & Competitor Position scores each package's booking velocity and occupancy, applies a seasonality/urgency curve based on days-to-departure, and compares list price against competitor average to compute a total recommended price adjustment\n3. Packages whose recommended adjustment is 3% or more are filtered through for repricing\n4. AI finalizes the price - applying sane rounding and a sanity cap - and writes a short reasoning note with a confidence level\n5. The new price and reasoning are appended to a PriceUpdates log for downstream systems and audit\n6. The revenue management team gets a Slack alert summarizing what changed and why\n\n### How to set up\n1. Import this workflow into n8n\n2. Add Anthropic Claude credentials and attach to the Anthropic model node\n3. Point Fetch Live Demand & Booking Data at your real booking system/API and add its auth header\n4. Point Fetch Competitor Pricing Data at your real competitor-pricing feed/API and add its auth header\n5. Switch the Google Sheets HTTP node to the native Google Sheets node with OAuth2, or add OAuth2 credentials directly\n6. Replace YOUR_SHEET_ID with your actual Spreadsheet ID\n7. Add Slack credentials and replace YOUR_SLACK_CHANNEL_ID\n8. Set the schedule interval to match how often demand/competitor data should be checked (default: every 30 minutes)\n9. Activate the workflow\n\n### Requirements\n- Anthropic Claude API key (claude-sonnet-4 recommended)\n- A booking/demand data API and a competitor pricing API\n- Google Sheets with OAuth2 credentials (PriceUpdates tab)\n- Slack workspace access for the revenue management alert\n\n### How to customize\n- Adjust the seasonality curve and competitor-adjustment thresholds in JS - Calculate Demand Score, Seasonality & Competitor Position\n- Change the 3% needsReprice threshold to reprice more or less frequently\n- Add a maximum single-run price-change cap so no departure jumps too far in one cycle\n- Add per-cabin/per-room-type tiers if your packages have multiple sellable fare classes\n\n### Google Sheets Column Layout\n\nPriceUpdates tab - row 1 headers:\nRepriced At | Package ID | Package Name | Base Price USD | Final Price USD | Price Change % | Demand Score | Seasonality Adj % | Competitor Adj % | Confidence | Reasoning"
},
"typeVersion": 1
},
{
"id": "9fd5f155-b06e-4787-b0ab-e709e66df65a",
"name": "Sticky Note - Stage 1",
"type": "n8n-nodes-base.stickyNote",
"position": [
1168,
64
],
"parameters": {
"color": 3,
"width": 1280,
"height": 860,
"content": "## Stage 1: Demand, Seasonality & Competitor Scoring\n\nSchedule Trigger polls on a fixed interval (default every 30 minutes). Two API calls run in parallel: Fetch Live Demand & Booking Data (API 1) pulls booking velocity, occupancy, and capacity per package; Fetch Competitor Pricing Data (API 2) pulls the current market average price per package.\n\nMerge - Combine Demand & Competitor Data joins both datasets into a single item by position.\n\nJS - Calculate Demand Score, Seasonality & Competitor Position (Code Node 1) computes days-to-departure and occupancy, a 0-100 demandScore from booking velocity, a seasonality/urgency adjustment based on how close departure is, and a competitor adjustment based on how far list price sits from the market average - combining all three into a single recommended price and a needsReprice flag.\n\nSplit Out - Packages turns that single array into one item per package so the rest of the workflow can evaluate each package independently."
},
"typeVersion": 1
},
{
"id": "ab5872c1-a127-4b66-9bc7-916f9e41ea91",
"name": "Sticky Note - Stage 2",
"type": "n8n-nodes-base.stickyNote",
"position": [
2544,
64
],
"parameters": {
"color": 4,
"width": 1108,
"height": 900,
"content": "## Stage 2: AI Yield Repricing Decision\n\nFilter - Packages Needing Reprice lets through only packages whose combined demand/seasonality/competitor adjustment is 3% or more, so stable packages are never touched.\n\nWait - API Rate Limit (Wait Node 1) respects API rate limits, then the yield-management AI (claude-sonnet-4, temperature 0.2 for consistent, conservative pricing) finalizes the price - applying sensible rounding and a sanity cap - and writes a one-line reasoning note plus a confidence level.\n\nJS - Parse AI Repricing Output (Code Node 2) extracts the structured JSON safely, with a fallback that uses the raw calculated recommendation if AI parsing fails.\n\nWait - Sheets Write Rate Limit (Wait Node 2) paces the write-back before it hits Google Sheets."
},
"typeVersion": 1
},
{
"id": "aead6ba3-926f-4459-adb3-39ae3aa57d62",
"name": "Sticky Note - Stage 3",
"type": "n8n-nodes-base.stickyNote",
"position": [
3728,
208
],
"parameters": {
"color": 5,
"width": 716,
"height": 620,
"content": "## Stage 3: Price Log & Alert\n\nJS - Format Repricing Update & Log Row (Code Node 3) builds the audit row: base price, final price, price change %, and the demand/seasonality/competitor factors that drove the decision.\n\nLog New Price to Google Sheets PriceUpdates appends that row - the living source of truth downstream systems can read to sync the live price.\n\nSend Slack Alert to Revenue Management Team posts a short summary so the revenue team has visibility into every autonomous price change as it happens."
},
"typeVersion": 1
},
{
"id": "5a1b3de0-8da5-4c86-86b5-f7087515c3c9",
"name": "Schedule Trigger - Poll Demand & Competitor Data",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
1200,
592
],
"parameters": {
"rule": {
"interval": [
{
"field": "minutes"
}
]
}
},
"typeVersion": 1.2
},
{
"id": "c21ae811-3d7d-44bd-b128-8d0a73a26d5c",
"name": "Fetch Live Demand & Booking Data",
"type": "n8n-nodes-base.httpRequest",
"position": [
1424,
464
],
"parameters": {
"url": "https://api.yourbookingsystem.example.com/v1/demand-snapshot",
"options": {},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "Bearer YOUR_TOKEN_HERE"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "ab48aad8-8602-43d9-bca1-696441cf8248",
"name": "Fetch Competitor Pricing Data",
"type": "n8n-nodes-base.httpRequest",
"position": [
1424,
720
],
"parameters": {
"url": "https://api.competitor-price-tracker.example.com/v1/prices",
"options": {},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "Bearer YOUR_TOKEN_HERE"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "6cab03ce-987e-4257-829c-7d4faa100e0d",
"name": "Merge - Combine Demand & Competitor Data",
"type": "n8n-nodes-base.merge",
"position": [
1648,
592
],
"parameters": {
"mode": "combine",
"options": {}
},
"typeVersion": 3.2
},
{
"id": "3e07351d-c0ae-4aa4-ba0e-28fd0c261aba",
"name": "JS - Calculate Demand Score, Seasonality & Competitor Position",
"type": "n8n-nodes-base.code",
"position": [
1872,
592
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const item = $input.item.json;\nconst packages = item.packages || []; // from demand API: [{ packageId, packageName, bookingsLast7Days, bookingsLast30Days, capacityRemaining, totalCapacity, departureDate, basePriceUSD, targetMarginPct }]\nconst competitors = item.competitors || []; // from competitor API: [{ packageId, competitorAvgPriceUSD, competitorMinPriceUSD, competitorCount }]\n\nconst competitorByPackage = {};\ncompetitors.forEach(c => { competitorByPackage[c.packageId] = c; });\n\nconst today = new Date();\n\nconst results = packages.map(p => {\n const departureDate = new Date(p.departureDate);\n const daysToDeparture = Math.max(0, Math.round((departureDate - today) / (1000 * 60 * 60 * 24)));\n\n const totalCapacity = p.totalCapacity || 0;\n const capacityRemaining = p.capacityRemaining || 0;\n const bookedCapacity = totalCapacity - capacityRemaining;\n const occupancyRate = totalCapacity > 0 ? Math.round((bookedCapacity / totalCapacity) * 100) : 0;\n\n const bookingVelocity = p.bookingsLast7Days || 0;\n const demandScore = Math.min(100, Math.round((bookingVelocity * 10) + (occupancyRate * 0.5)));\n\n // Seasonality / urgency curve based on days-to-departure and current occupancy\n let seasonalityFactor = 0;\n if (daysToDeparture <= 14) {\n seasonalityFactor = occupancyRate >= 80 ? 8 : -10;\n } else if (daysToDeparture <= 45) {\n seasonalityFactor = occupancyRate >= 70 ? 5 : -4;\n } else {\n seasonalityFactor = occupancyRate >= 60 ? 3 : 0;\n }\n\n const competitor = competitorByPackage[p.packageId] || null;\n const competitorAvgPriceUSD = competitor ? competitor.competitorAvgPriceUSD : null;\n const competitorPriceIndex = (competitorAvgPriceUSD && p.basePriceUSD)\n ? Math.round(((p.basePriceUSD - competitorAvgPriceUSD) / competitorAvgPriceUSD) * 1000) / 10\n : null; // positive = priced above market, negative = priced below market\n\n let competitorAdjustmentPct = 0;\n if (competitorPriceIndex !== null) {\n if (competitorPriceIndex > 8) competitorAdjustmentPct = -5;\n else if (competitorPriceIndex < -8) competitorAdjustmentPct = 4;\n }\n\n const totalAdjustmentPct = Math.round((seasonalityFactor + competitorAdjustmentPct + (demandScore - 50) * 0.1) * 10) / 10;\n const recommendedPriceUSD = p.basePriceUSD ? Math.round(p.basePriceUSD * (1 + totalAdjustmentPct / 100) * 100) / 100 : null;\n const needsReprice = Math.abs(totalAdjustmentPct) >= 3;\n\n return {\n packageId: p.packageId || '',\n packageName: p.packageName || '',\n departureDate: p.departureDate || '',\n daysToDeparture,\n totalCapacity,\n capacityRemaining,\n occupancyRate,\n bookingsLast7Days: p.bookingsLast7Days || 0,\n bookingsLast30Days: p.bookingsLast30Days || 0,\n basePriceUSD: p.basePriceUSD || 0,\n targetMarginPct: p.targetMarginPct || 0,\n demandScore,\n seasonalityFactor,\n competitorAvgPriceUSD,\n competitorPriceIndex,\n competitorAdjustmentPct,\n totalAdjustmentPct,\n recommendedPriceUSD,\n needsReprice\n };\n});\n\nreturn { json: { results, checkedAt: new Date().toISOString() } };"
},
"typeVersion": 2
},
{
"id": "431dcdf8-d26d-4471-be9a-646f0ce6d0ad",
"name": "Split Out - Packages",
"type": "n8n-nodes-base.splitOut",
"position": [
2096,
592
],
"parameters": {
"options": {},
"fieldToSplitOut": "results"
},
"typeVersion": 1
},
{
"id": "1b64ede4-7cb4-4a6a-9fee-ceb91be625e1",
"name": "Filter - Packages Needing Reprice",
"type": "n8n-nodes-base.filter",
"position": [
2320,
592
],
"parameters": {
"options": {},
"conditions": {
"conditions": [
{
"operator": {
"type": "boolean",
"operation": "true"
},
"leftValue": "={{ $json.needsReprice === true }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.2
},
{
"id": "21498f36-0d65-4161-ad89-ba97f5bae927",
"name": "AI - Generate Yield Repricing Decision",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
2912,
592
],
"parameters": {
"text": "=You are a revenue management analyst applying airline-style yield management to tour packages.\n\nPackage: {{ $json.packageName }} ({{ $json.packageId }})\nDeparture: {{ $json.departureDate }} ({{ $json.daysToDeparture }} days out)\nOccupancy: {{ $json.occupancyRate }}% ({{ $json.capacityRemaining }} of {{ $json.totalCapacity }} seats remaining)\nBooking Velocity: {{ $json.bookingsLast7Days }} bookings in last 7 days\nDemand Score: {{ $json.demandScore }}/100\nSeasonality/Urgency Adjustment: {{ $json.seasonalityFactor }}%\nCompetitor Avg Price: {{ $json.competitorAvgPriceUSD }} USD (we are {{ $json.competitorPriceIndex }}% vs market)\nCompetitor Adjustment: {{ $json.competitorAdjustmentPct }}%\nCurrent Base Price: {{ $json.basePriceUSD }} USD\nTarget Margin: {{ $json.targetMarginPct }}%\nCalculated Total Adjustment: {{ $json.totalAdjustmentPct }}% -> Recommended Price: {{ $json.recommendedPriceUSD }} USD\n\nFinalize the price. Apply sensible psychological rounding (e.g. end in .00 or .99) and never move price more than 15% from the current base price in a single cycle, even if the calculated adjustment suggests more. Write one short sentence explaining the decision and a confidence level.\n\nReturn a JSON object with EXACTLY these fields:\n{\n \"finalPriceUSD\": number,\n \"priceChangePct\": number,\n \"reasoning\": \"One short sentence for internal audit\",\n \"confidence\": \"High, Medium, or Low\"\n}\n\nReturn ONLY the JSON. No markdown, no explanation.",
"options": {},
"promptType": "define"
},
"typeVersion": 1.6
},
{
"id": "9ddb594f-797d-4a68-943e-831ebb6323f0",
"name": "Anthropic - Yield Repricing Model",
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
"position": [
2768,
800
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "claude-sonnet-4-20250514"
},
"options": {
"temperature": 0.2
}
},
"credentials": {
"anthropicApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.3
},
{
"id": "d6e4a012-47ff-4123-bb07-a626089b5800",
"name": "JS - Parse AI Repricing Output",
"type": "n8n-nodes-base.code",
"position": [
3248,
592
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const item = $input.item.json;\nlet parsed = {};\ntry {\n const rawText = item.output || item.text || item.response || '{}';\n const clean = rawText.replace(/```json|```/g, '').trim();\n parsed = JSON.parse(clean);\n} catch (e) {\n parsed = {\n finalPriceUSD: item.recommendedPriceUSD,\n priceChangePct: item.totalAdjustmentPct,\n reasoning: `Auto-adjusted from demand score ${item.demandScore}, seasonality ${item.seasonalityFactor}%, competitor index ${item.competitorPriceIndex}%.`,\n confidence: 'Medium'\n };\n}\nreturn {\n json: {\n ...item,\n ...parsed\n }\n};"
},
"typeVersion": 2
},
{
"id": "a315d200-6053-481a-a97c-0e5b3eeff5ce",
"name": "JS - Format Repricing Update & Log Row",
"type": "n8n-nodes-base.code",
"position": [
3808,
592
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const item = $input.item.json;\nconst repricedAt = new Date().toISOString();\n\nconst finalPriceUSD = (item.finalPriceUSD !== undefined && item.finalPriceUSD !== null) ? item.finalPriceUSD : item.recommendedPriceUSD;\nconst priceChangePct = (item.priceChangePct !== undefined && item.priceChangePct !== null) ? item.priceChangePct : item.totalAdjustmentPct;\n\nreturn {\n json: {\n ...item,\n finalPriceUSD,\n priceChangePct,\n repricedAt,\n repricingLogRow: [\n repricedAt,\n item.packageId || '',\n item.packageName || '',\n item.basePriceUSD || 0,\n finalPriceUSD,\n priceChangePct,\n item.demandScore || 0,\n item.seasonalityFactor || 0,\n item.competitorAdjustmentPct || 0,\n item.confidence || '',\n item.reasoning || ''\n ]\n }\n};"
},
"typeVersion": 2
},
{
"id": "010f6bba-5d4d-480d-99ce-dcbad30fe51e",
"name": "Log New Price to Google Sheets PriceUpdates",
"type": "n8n-nodes-base.httpRequest",
"position": [
4032,
592
],
"parameters": {
"url": "https://sheets.googleapis.com/v4/spreadsheets/YOUR_SHEET_ID/values/PriceUpdates!A1:append?valueInputOption=USER_ENTERED",
"method": "POST",
"options": {},
"jsonBody": "={\n \"values\": [{{ JSON.stringify($json.repricingLogRow) }}]\n}",
"sendBody": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "oAuth2Api"
},
"credentials": {
"oAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "442c25c0-fb3b-46ca-a3e5-16095f192a53",
"name": "Send Slack Alert to Revenue Management Team",
"type": "n8n-nodes-base.slack",
"position": [
4256,
592
],
"parameters": {
"text": "=:chart_with_upwards_trend: *Repriced* {{ $json.packageName }} ({{ $json.packageId }})\nDeparture in {{ $json.daysToDeparture }} days, {{ $json.occupancyRate }}% occupied\nPrice: {{ $json.basePriceUSD }} USD -> {{ $json.finalPriceUSD }} USD ({{ $json.priceChangePct }}%)\nConfidence: {{ $json.confidence }}\nReason: {{ $json.reasoning }}",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "id",
"value": "=YOUR_SLACK_CHANNEL_ID"
},
"otherOptions": {}
},
"credentials": {
"slackApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.2
},
{
"id": "2af41452-05f4-4cf3-966e-c64de57f3aa7",
"name": "Wait For Limit",
"type": "n8n-nodes-base.wait",
"position": [
2624,
592
],
"parameters": {},
"typeVersion": 1
},
{
"id": "b6745b4b-976e-4fb6-a2d6-7216fb1b439d",
"name": "Wait For Write Rate Limit",
"type": "n8n-nodes-base.wait",
"position": [
3472,
592
],
"parameters": {},
"typeVersion": 1.1
}
],
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "2f3d5598-9867-4a52-9801-05c53e4f8f72",
"connections": {
"Wait For Limit": {
"main": [
[
{
"node": "AI - Generate Yield Repricing Decision",
"type": "main",
"index": 0
}
]
]
},
"Split Out - Packages": {
"main": [
[
{
"node": "Filter - Packages Needing Reprice",
"type": "main",
"index": 0
}
]
]
},
"Wait For Write Rate Limit": {
"main": [
[
{
"node": "JS - Format Repricing Update & Log Row",
"type": "main",
"index": 0
}
]
]
},
"Fetch Competitor Pricing Data": {
"main": [
[
{
"node": "Merge - Combine Demand & Competitor Data",
"type": "main",
"index": 1
}
]
]
},
"JS - Parse AI Repricing Output": {
"main": [
[
{
"node": "Wait For Write Rate Limit",
"type": "main",
"index": 0
}
]
]
},
"Fetch Live Demand & Booking Data": {
"main": [
[
{
"node": "Merge - Combine Demand & Competitor Data",
"type": "main",
"index": 0
}
]
]
},
"Anthropic - Yield Repricing Model": {
"ai_languageModel": [
[
{
"node": "AI - Generate Yield Repricing Decision",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Filter - Packages Needing Reprice": {
"main": [
[
{
"node": "Wait For Limit",
"type": "main",
"index": 0
}
]
]
},
"AI - Generate Yield Repricing Decision": {
"main": [
[
{
"node": "JS - Parse AI Repricing Output",
"type": "main",
"index": 0
}
]
]
},
"JS - Format Repricing Update & Log Row": {
"main": [
[
{
"node": "Log New Price to Google Sheets PriceUpdates",
"type": "main",
"index": 0
}
]
]
},
"Merge - Combine Demand & Competitor Data": {
"main": [
[
{
"node": "JS - Calculate Demand Score, Seasonality & Competitor Position",
"type": "main",
"index": 0
}
]
]
},
"Log New Price to Google Sheets PriceUpdates": {
"main": [
[
{
"node": "Send Slack Alert to Revenue Management Team",
"type": "main",
"index": 0
}
]
]
},
"Schedule Trigger - Poll Demand & Competitor Data": {
"main": [
[
{
"node": "Fetch Live Demand & Booking Data",
"type": "main",
"index": 0
},
{
"node": "Fetch Competitor Pricing Data",
"type": "main",
"index": 0
}
]
]
},
"JS - Calculate Demand Score, Seasonality & Competitor Position": {
"main": [
[
{
"node": "Split Out - Packages",
"type": "main",
"index": 0
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
anthropicApioAuth2ApislackApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow runs on a schedule to fetch live tour-package demand signals and competitor prices via HTTP APIs, calculates repricing recommendations, uses Anthropic Claude to finalize capped and rounded price changes, then logs each update to Google Sheets and notifies a Slack…
Source: https://n8n.io/workflows/17359/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
This n8n template builds an automated daily news digest powered by Claude AI.
This workflow automates engineering governance by deploying a multi-agent AI system that validates designs, checks compliance, optimises safety, and predicts maintenance needs. Designed for engineerin
This advanced n8n workflow is designed for web developers, system administrators, security analysts, and agency owners who need to automate the monitoring of website security posture. It acts as a vir
Activate this workflow once and every day at 9AM it automatically processes all Zoom meetings from the past 24 hours — no manual action needed after any call. For each recorded meeting, it downloads t
This workflow is designed for Google Ads advertisers, PPC managers, and marketing teams who want to automatically monitor and optimize their search term performance. It's particularly valuable for tho