rtrvr.ai API: Competitor/Company Analysis
"Analyze these 5 companies—products, pricing, customer segments."
I typed that into Slack. 30 seconds later, structured intel on all 5 landed right back in the channel.
Here's what happened under the hood:
→ Slack triggered an n8n workflow
→ n8n called the rtrvr.ai API
→ 5 cloud browsers spun up in parallel
→ Each navigated, extracted, and returned structured data
→ Results pushed back to Slack
No Puppeteer scripts. No brittle selectors. Just a prompt.
Today we're launching the rtrvr.ai API.
The technical foundation that makes this work:
Parallel cloud browser execution: Spin up hundreds of browser agents simultaneously. What used to take hours of sequential scraping now happens in seconds.
I set up slack with n8n 1st node listens to channel messages including the rtrvr-n8n bot and you can ask to analyze any number of companies and any data about them. this wil trugger 2nd node basic ai node that parses messages and gets it ready for rtrvr.ai API request body. 3rd node is code code cleans up AI response and passes it to http node which actually makes rtrvr.ai API call. 5th node takes output from rtrvr.ai API call and extract the text message to be sent to the final Slack node that writes back to the channel.
Shared 11/27/2025
11 views
Visual Workflow
JSON Code
{
"id": "SZFFDLCOIM6MThvz",
"meta": {
"instanceId": "b1bb7ff635e00d1cc037cbba807e26bf1b14585a432f149eb4ce99cc7ee0430f",
"templateCredsSetupCompleted": true
},
"name": "Competitor Analysis",
"tags": [],
"nodes": [
{
"id": "ce44e866-2a7b-4c8e-8e3b-0dc9405ec52e",
"name": "Slack Trigger",
"type": "n8n-nodes-base.slackTrigger",
"position": [
0,
0
],
"webhookId": "c0c68d74-6d31-4531-9ed2-0617a9e5f6bf",
"parameters": {
"options": {},
"trigger": [
"app_mention"
],
"channelId": {
"__rl": true,
"mode": "list",
"value": "C09UV6197LP",
"cachedResultName": "sales"
}
},
"credentials": {
"slackApi": {
"id": "Gk8cIlFwYqmlRDc2",
"name": "Slack account"
}
},
"typeVersion": 1
},
{
"id": "45daf3d0-af26-4c96-a2ff-d2d1f55fed87",
"name": "Message a model",
"type": "@n8n/n8n-nodes-langchain.googleGemini",
"position": [
208,
0
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "models/gemini-2.5-flash",
"cachedResultName": "models/gemini-2.5-flash"
},
"options": {},
"messages": {
"values": [
{
"content": "={{ $json.text }}"
},
{
"role": "model",
"content": "You are a data extraction assistant. Extract company information from user messages.\n\nYour task:\n1. Extract all company identifiers mentioned\n2. Convert company names to their primary website URLs when you know them\n3. Extract the user's analysis request (what they want to know)\n\nReturn ONLY valid JSON in this exact format:\n{\n \"companies\": [\"identifier1\", \"identifier2\", \"...\"],\n \"userRequest\": \"the user's request\"\n}\n\nRules for company identifiers:\n- If user provides a URL or domain (e.g., \"stripe.com\", \"https://openai.com\") → keep it EXACTLY as-is\n- If user provides a company name AND you know the official website → convert to URL (e.g., \"OpenAI\" → \"openai.com\")\n- If user provides a company name AND you're unsure of the URL → keep the name as-is\n- Remove http://, https://, www. prefixes from URLs (e.g., \"https://www.stripe.com\" → \"stripe.com\")\n- Use the primary domain, not subdomains (e.g., \"docs.stripe.com\" → \"stripe.com\")\n- Keep domains lowercase\n\nExamples:\n- \"Analyze OpenAI, Anthropic, and Mistral\" → [\"openai.com\", \"anthropic.com\", \"mistral.ai\"]\n- \"Check stripe.com and Square\" → [\"stripe.com\", \"squareup.com\"]\n- \"Compare https://netflix.com and Hulu\" → [\"netflix.com\", \"hulu.com\"]\n- \"Look at Acme Corp and Example Inc\" → [\"Acme Corp\", \"Example Inc\"] (unknown companies, keep names)\n\nIf no companies found, return empty array.\nCapture the essence of what the user wants to analyze in \"userRequest\"."
}
]
},
"jsonOutput": true
},
"credentials": {
"googlePalmApi": {
"id": "yWQERgtMZAQZhPid",
"name": "Google Gemini(PaLM) Api account"
}
},
"typeVersion": 1
},
{
"id": "11f87727-ee77-4c33-853d-af89910e7813",
"name": "Send a message",
"type": "n8n-nodes-base.slack",
"position": [
1184,
0
],
"webhookId": "15ebddcb-4a22-48bc-84d4-c684079b57ae",
"parameters": {
"text": "={{ $json.slackMessage }}",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C09UV6197LP",
"cachedResultName": "sales"
},
"otherOptions": {}
},
"credentials": {
"slackApi": {
"id": "Gk8cIlFwYqmlRDc2",
"name": "Slack account"
}
},
"typeVersion": 2.3
},
{
"id": "b952995d-3764-4448-abd8-e4507b33ceb1",
"name": "Code in JavaScript",
"type": "n8n-nodes-base.code",
"position": [
560,
0
],
"parameters": {
"jsCode": "// Get Gemini response text\nconst geminiText = $input.first().json.content.parts[0].text;\n\n// Strip markdown code fences if present (```json ... ```)\nlet cleanText = geminiText.trim();\nif (cleanText.startsWith('```json')) {\n cleanText = cleanText.replace(/```json\\s*/g, '').replace(/```\\s*$/g, '');\n} else if (cleanText.startsWith('```')) {\n cleanText = cleanText.replace(/```\\s*/g, '').replace(/```\\s*$/g, '');\n}\n\n// Parse the JSON string\nconst parsed = JSON.parse(cleanText);\n\n// Get parsed data\nconst companies = parsed.companies || [];\nconst userRequest = parsed.userRequest || \"\";\n\n// Build inline CSV from companies\nlet csvInline = \"company\\n\";\nfor (const company of companies) {\n csvInline += `${company}\\n`;\n}\n\n// Construct the rtrvr API request body\nconst apiRequestBody = {\n input: `${userRequest}. For each company in the sheet, analyze:\n1) Main products/services\n2) Pricing model\n3) Key differentiation\n\nReturn structured JSON array: [{\"company\": \"...\", \"products\": \"...\", \"pricing\": \"...\", \"target_customer\": \"...\", \"differentiation\": \"...\"}]`,\n \n dataInputs: [\n {\n description: \"List of companies to analyze\",\n format: \"csv\",\n inline: csvInline\n }\n ],\n \n response: {\n verbosity: \"final\"\n }\n};\n\n// Keep Slack metadata for reply later\nreturn {\n json: {\n apiRequestBody: apiRequestBody,\n slackChannel: $('Slack Trigger').first().json.channel,\n slackThreadTs: $('Slack Trigger').first().json.ts,\n slackUser: $('Slack Trigger').first().json.user // Fixed typo: 'user' not 'users'\n }\n};"
},
"typeVersion": 2
},
{
"id": "8b4d81ff-5437-4324-b883-5a27654adfd5",
"name": "HTTP Request",
"type": "n8n-nodes-base.httpRequest",
"position": [
768,
0
],
"parameters": {
"url": "https://api.rtrvr.ai/execute",
"method": "POST",
"options": {
"timeout": 600000,
"response": {
"response": {
"responseFormat": "json"
}
}
},
"jsonBody": "={{ $json.apiRequestBody }}",
"sendBody": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth"
},
"credentials": {
"httpHeaderAuth": {
"id": "xbxd9RYwx2IEeG5C",
"name": "Header Auth account"
}
},
"typeVersion": 4.3
},
{
"id": "31e1ce6e-8a16-4d0b-8fff-6e1dea681a9e",
"name": "Code in JavaScript1",
"type": "n8n-nodes-base.code",
"position": [
976,
0
],
"parameters": {
"jsCode": "// Get the rtrvr API response\nconst apiResponse = $('HTTP Request').first().json;\n\n// Get Slack metadata from previous node\nconst slackChannel = $('Code in JavaScript').first().json.slackChannel;\nconst slackThreadTs = $('Code in JavaScript').first().json.slackThreadTs;\n\n// Check if API call was successful\nif (!apiResponse.success || !apiResponse.output) {\n return {\n json: {\n slackChannel: slackChannel,\n slackThreadTs: slackThreadTs,\n slackMessage: \"❌ Sorry, the analysis failed. Please try again.\"\n }\n };\n}\n\n// Helper function to format values nicely\nfunction formatValue(value) {\n if (value === null || value === undefined) return \"N/A\";\n if (typeof value === 'string') return value;\n if (Array.isArray(value)) return value.join(\", \");\n if (typeof value === 'object') return JSON.stringify(value, null, 2);\n return String(value);\n}\n\n// Helper function to format key names nicely\nfunction formatKey(key) {\n return key\n .split('_')\n .map(word => word.charAt(0).toUpperCase() + word.slice(1))\n .join(' ');\n}\n\n// Process output blocks\nconst outputBlocks = apiResponse.output || [];\nlet slackMessage = \"*🔍 Analysis Results*\\n\\n\";\n\nif (outputBlocks.length === 0) {\n slackMessage += \"No output was returned.\";\n} else {\n for (const block of outputBlocks) {\n // Handle text blocks\n if (block.type === 'text' && block.text) {\n slackMessage += `${block.text}\\n\\n`;\n }\n \n // Handle JSON blocks\n else if (block.type === 'json' && block.data) {\n const data = block.data;\n \n // Normalize to array\n let dataArray = Array.isArray(data) ? data : [data];\n \n for (const item of dataArray) {\n // Handle string items\n if (typeof item === 'string') {\n slackMessage += `${item}\\n\\n`;\n continue;\n }\n \n // Handle object items\n if (typeof item === 'object' && item !== null) {\n // Get the first key as header\n const keys = Object.keys(item);\n if (keys.length > 0) {\n const headerKey = keys[0];\n const headerValue = item[headerKey];\n slackMessage += `*${formatValue(headerValue)}*\\n`;\n \n // Add all other fields\n for (const [key, value] of Object.entries(item)) {\n if (key === headerKey) continue;\n \n const formattedKey = formatKey(key);\n const formattedValue = formatValue(value);\n \n slackMessage += `• *${formattedKey}:* ${formattedValue}\\n`;\n }\n } else {\n slackMessage += `${JSON.stringify(item, null, 2)}\\n`;\n }\n \n slackMessage += `\\n`;\n } else {\n slackMessage += `${formatValue(item)}\\n\\n`;\n }\n }\n }\n \n // Handle tool_result blocks (if they have output)\n else if (block.type === 'tool_result' && block.output) {\n slackMessage += `*Tool: ${block.toolName}*\\n`;\n slackMessage += `${formatValue(block.output)}\\n\\n`;\n }\n }\n}\n\n// Add trajectory ID for reference\nif (apiResponse.trajectoryId) {\n slackMessage += `\\n_Analysis ID: ${apiResponse.trajectoryId}_`;\n}\n\nreturn {\n json: {\n slackChannel: slackChannel,\n slackThreadTs: slackThreadTs,\n slackMessage: slackMessage\n }\n};"
},
"typeVersion": 2
}
],
"active": true,
"pinData": {},
"settings": {
"executionOrder": "v1"
},
"versionId": "8fdc0ca8-a816-4f9f-a419-f9c9bc745e80",
"connections": {
"HTTP Request": {
"main": [
[
{
"node": "Code in JavaScript1",
"type": "main",
"index": 0
}
]
]
},
"Slack Trigger": {
"main": [
[
{
"node": "Message a model",
"type": "main",
"index": 0
}
]
]
},
"Message a model": {
"main": [
[
{
"node": "Code in JavaScript",
"type": "main",
"index": 0
}
]
]
},
"Code in JavaScript": {
"main": [
[
{
"node": "HTTP Request",
"type": "main",
"index": 0
}
]
]
},
"Code in JavaScript1": {
"main": [
[
{
"node": "Send a message",
"type": "main",
"index": 0
}
]
]
}
}
}