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 →
{
"name": "6ixo - Crawl4AI Listings to CSV",
"nodes": [
{
"parameters": {},
"id": "manual-trigger",
"name": "Manual Trigger",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
120,
220
]
},
{
"parameters": {
"rule": {
"interval": [
{
"field": "hours",
"hoursInterval": 6
}
]
}
},
"id": "schedule-trigger",
"name": "Every 6 Hours",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
120,
420
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "githubToken",
"name": "githubToken",
"value": "PASTE_GITHUB_TOKEN_HERE",
"type": "string"
},
{
"id": "githubOwner",
"name": "githubOwner",
"value": "bisco401",
"type": "string"
},
{
"id": "githubRepo",
"name": "githubRepo",
"value": "6ixo",
"type": "string"
},
{
"id": "githubBranch",
"name": "githubBranch",
"value": "main",
"type": "string"
},
{
"id": "csvPath",
"name": "csvPath",
"value": "data/scraped-listings.csv",
"type": "string"
},
{
"id": "defaultImportStatus",
"name": "defaultImportStatus",
"value": "published",
"type": "string"
},
{
"id": "crawl4aiUrl",
"name": "crawl4aiUrl",
"value": "http://10.0.0.164:11235/crawl",
"type": "string"
},
{
"id": "sourcesJson",
"name": "sourcesJson",
"value": "[\n {\n \"name\": \"JACars Cars\",\n \"enabled\": true,\n \"list_url\": \"https://www.jacars.net/cars/\",\n \"base_url\": \"https://www.jacars.net\",\n \"target_surface\": \"vehicles\",\n \"app_category\": \"vehicles\",\n \"app_subcategory\": \"vehicles\",\n \"rate_limit_seconds\": 3,\n \"extractor_config\": {\n \"parser\": \"jacars\",\n \"country\": \"Jamaica\",\n \"crawl_detail_pages\": true,\n \"detail_batch_size\": 10,\n \"require_vehicle_year\": true,\n \"recent_only\": true,\n \"max_age_hours\": 48,\n \"max_listings\": 20\n }\n },\n {\n \"name\": \"JACars Car Parts\",\n \"enabled\": true,\n \"list_url\": \"https://www.jacars.net/car-parts/car-parts/\",\n \"base_url\": \"https://www.jacars.net\",\n \"target_surface\": \"vehicles\",\n \"app_category\": \"vehicles\",\n \"app_subcategory\": \"auto_parts\",\n \"rate_limit_seconds\": 3,\n \"extractor_config\": {\n \"parser\": \"jacars\",\n \"country\": \"Jamaica\",\n \"crawl_detail_pages\": true,\n \"detail_batch_size\": 10,\n \"require_vehicle_year\": false,\n \"recent_only\": true,\n \"max_age_hours\": 720,\n \"max_listings\": 20\n }\n },\n {\n \"name\": \"JACars Car Accessories\",\n \"enabled\": true,\n \"list_url\": \"https://www.jacars.net/car-parts/car-accessories/\",\n \"base_url\": \"https://www.jacars.net\",\n \"target_surface\": \"vehicles\",\n \"app_category\": \"vehicles\",\n \"app_subcategory\": \"auto_parts\",\n \"rate_limit_seconds\": 3,\n \"extractor_config\": {\n \"parser\": \"jacars\",\n \"country\": \"Jamaica\",\n \"crawl_detail_pages\": true,\n \"detail_batch_size\": 10,\n \"require_vehicle_year\": false,\n \"recent_only\": true,\n \"max_age_hours\": 720,\n \"max_listings\": 20\n }\n },\n {\n \"name\": \"JACars Tires and Rims\",\n \"enabled\": true,\n \"list_url\": \"https://www.jacars.net/car-parts/tyres-and-rims/\",\n \"base_url\": \"https://www.jacars.net\",\n \"target_surface\": \"vehicles\",\n \"app_category\": \"vehicles\",\n \"app_subcategory\": \"tires_rims\",\n \"rate_limit_seconds\": 3,\n \"extractor_config\": {\n \"parser\": \"jacars\",\n \"country\": \"Jamaica\",\n \"crawl_detail_pages\": true,\n \"detail_batch_size\": 10,\n \"require_vehicle_year\": false,\n \"recent_only\": true,\n \"max_age_hours\": 720,\n \"max_listings\": 20\n }\n },\n {\n \"name\": \"JACars Auto Services\",\n \"enabled\": true,\n \"list_url\": \"https://www.jacars.net/services/\",\n \"base_url\": \"https://www.jacars.net\",\n \"target_surface\": \"vehicles\",\n \"app_category\": \"vehicles\",\n \"app_subcategory\": \"repairs\",\n \"rate_limit_seconds\": 3,\n \"extractor_config\": {\n \"parser\": \"jacars\",\n \"country\": \"Jamaica\",\n \"crawl_detail_pages\": true,\n \"detail_batch_size\": 10,\n \"require_vehicle_year\": false,\n \"recent_only\": true,\n \"max_age_hours\": 720,\n \"max_listings\": 20\n }\n },\n {\n \"enabled\": true,\n \"base_url\": \"https://www.jacars.net\",\n \"target_surface\": \"vehicles\",\n \"app_category\": \"vehicles\",\n \"rate_limit_seconds\": 3,\n \"name\": \"JACars Alarm Systems and Security\",\n \"list_url\": \"https://www.jacars.net/car-parts/alarm-systems/\",\n \"app_subcategory\": \"auto_parts\",\n \"extractor_config\": {\n \"parser\": \"jacars\",\n \"country\": \"Jamaica\",\n \"crawl_detail_pages\": true,\n \"detail_batch_size\": 10,\n \"require_vehicle_year\": false,\n \"recent_only\": true,\n \"max_age_hours\": 720,\n \"max_listings\": 20\n }\n },\n {\n \"enabled\": true,\n \"base_url\": \"https://www.jacars.net\",\n \"target_surface\": \"vehicles\",\n \"app_category\": \"vehicles\",\n \"rate_limit_seconds\": 3,\n \"name\": \"JACars Audio Visual Electronics\",\n \"list_url\": \"https://www.jacars.net/car-parts/car-audio-and-visual/\",\n \"app_subcategory\": \"auto_parts\",\n \"extractor_config\": {\n \"parser\": \"jacars\",\n \"country\": \"Jamaica\",\n \"crawl_detail_pages\": true,\n \"detail_batch_size\": 10,\n \"require_vehicle_year\": false,\n \"recent_only\": true,\n \"max_age_hours\": 720,\n \"max_listings\": 20\n }\n },\n {\n \"enabled\": true,\n \"base_url\": \"https://www.jacars.net\",\n \"target_surface\": \"vehicles\",\n \"app_category\": \"vehicles\",\n \"rate_limit_seconds\": 3,\n \"name\": \"JACars Crashed Cars for Spares\",\n \"list_url\": \"https://www.jacars.net/car-parts/crashed-cars-for-spares/\",\n \"app_subcategory\": \"auto_parts\",\n \"extractor_config\": {\n \"parser\": \"jacars\",\n \"country\": \"Jamaica\",\n \"crawl_detail_pages\": true,\n \"detail_batch_size\": 10,\n \"require_vehicle_year\": false,\n \"recent_only\": true,\n \"max_age_hours\": 720,\n \"max_listings\": 20\n }\n },\n {\n \"enabled\": true,\n \"base_url\": \"https://www.jacars.net\",\n \"target_surface\": \"vehicles\",\n \"app_category\": \"vehicles\",\n \"rate_limit_seconds\": 3,\n \"name\": \"JACars Other Car Parts\",\n \"list_url\": \"https://www.jacars.net/car-parts/other/\",\n \"app_subcategory\": \"auto_parts\",\n \"extractor_config\": {\n \"parser\": \"jacars\",\n \"country\": \"Jamaica\",\n \"crawl_detail_pages\": true,\n \"detail_batch_size\": 10,\n \"require_vehicle_year\": false,\n \"recent_only\": true,\n \"max_age_hours\": 720,\n \"max_listings\": 20\n }\n },\n {\n \"enabled\": true,\n \"base_url\": \"https://www.jacars.net\",\n \"target_surface\": \"vehicles\",\n \"app_category\": \"vehicles\",\n \"rate_limit_seconds\": 3,\n \"name\": \"JACars Tools\",\n \"list_url\": \"https://www.jacars.net/car-parts/tools/\",\n \"app_subcategory\": \"auto_parts\",\n \"extractor_config\": {\n \"parser\": \"jacars\",\n \"country\": \"Jamaica\",\n \"crawl_detail_pages\": true,\n \"detail_batch_size\": 10,\n \"require_vehicle_year\": false,\n \"recent_only\": true,\n \"max_age_hours\": 720,\n \"max_listings\": 20\n }\n },\n {\n \"enabled\": true,\n \"base_url\": \"https://www.jacars.net\",\n \"target_surface\": \"vehicles\",\n \"app_category\": \"vehicles\",\n \"rate_limit_seconds\": 3,\n \"name\": \"JACars Luggage Racks Tow Bars\",\n \"list_url\": \"https://www.jacars.net/car-parts/luggage-racks-tow-bars/\",\n \"app_subcategory\": \"auto_parts\",\n \"extractor_config\": {\n \"parser\": \"jacars\",\n \"country\": \"Jamaica\",\n \"crawl_detail_pages\": true,\n \"detail_batch_size\": 10,\n \"require_vehicle_year\": false,\n \"recent_only\": true,\n \"max_age_hours\": 720,\n \"max_listings\": 20\n }\n },\n {\n \"enabled\": true,\n \"base_url\": \"https://www.jacars.net\",\n \"target_surface\": \"vehicles\",\n \"app_category\": \"vehicles\",\n \"rate_limit_seconds\": 3,\n \"name\": \"JACars Auto Repair Shops\",\n \"list_url\": \"https://www.jacars.net/services/auto-repair-shops/\",\n \"app_subcategory\": \"repairs\",\n \"extractor_config\": {\n \"parser\": \"jacars\",\n \"country\": \"Jamaica\",\n \"crawl_detail_pages\": true,\n \"detail_batch_size\": 10,\n \"require_vehicle_year\": false,\n \"recent_only\": true,\n \"max_age_hours\": 720,\n \"max_listings\": 20\n }\n },\n {\n \"enabled\": true,\n \"base_url\": \"https://www.jacars.net\",\n \"target_surface\": \"vehicles\",\n \"app_category\": \"vehicles\",\n \"rate_limit_seconds\": 3,\n \"name\": \"JACars Body Repairs Paint Shops\",\n \"list_url\": \"https://www.jacars.net/services/body-repairs-paint-shops/\",\n \"app_subcategory\": \"repairs\",\n \"extractor_config\": {\n \"parser\": \"jacars\",\n \"country\": \"Jamaica\",\n \"crawl_detail_pages\": true,\n \"detail_batch_size\": 10,\n \"require_vehicle_year\": false,\n \"recent_only\": true,\n \"max_age_hours\": 720,\n \"max_listings\": 20\n }\n },\n {\n \"enabled\": true,\n \"base_url\": \"https://www.jacars.net\",\n \"target_surface\": \"vehicles\",\n \"app_category\": \"vehicles\",\n \"rate_limit_seconds\": 3,\n \"name\": \"JACars Tuning Shops\",\n \"list_url\": \"https://www.jacars.net/services/repairing-tuning/\",\n \"app_subcategory\": \"repairs\",\n \"extractor_config\": {\n \"parser\": \"jacars\",\n \"country\": \"Jamaica\",\n \"crawl_detail_pages\": true,\n \"detail_batch_size\": 10,\n \"require_vehicle_year\": false,\n \"recent_only\": true,\n \"max_age_hours\": 720,\n \"max_listings\": 20\n }\n },\n {\n \"enabled\": true,\n \"base_url\": \"https://www.jacars.net\",\n \"target_surface\": \"vehicles\",\n \"app_category\": \"vehicles\",\n \"rate_limit_seconds\": 3,\n \"name\": \"JACars Car Rentals\",\n \"list_url\": \"https://www.jacars.net/services/car-hire/\",\n \"app_subcategory\": \"rentals\",\n \"extractor_config\": {\n \"parser\": \"jacars\",\n \"country\": \"Jamaica\",\n \"crawl_detail_pages\": true,\n \"detail_batch_size\": 10,\n \"require_vehicle_year\": false,\n \"recent_only\": true,\n \"max_age_hours\": 720,\n \"max_listings\": 20\n }\n },\n {\n \"enabled\": true,\n \"base_url\": \"https://www.jacars.net\",\n \"target_surface\": \"vehicles\",\n \"app_category\": \"vehicles\",\n \"rate_limit_seconds\": 3,\n \"name\": \"JACars Importers Dealers\",\n \"list_url\": \"https://www.jacars.net/services/importers-dealers/\",\n \"app_subcategory\": \"other\",\n \"extractor_config\": {\n \"parser\": \"jacars\",\n \"country\": \"Jamaica\",\n \"crawl_detail_pages\": true,\n \"detail_batch_size\": 10,\n \"require_vehicle_year\": false,\n \"recent_only\": true,\n \"max_age_hours\": 720,\n \"max_listings\": 20\n }\n },\n {\n \"enabled\": true,\n \"base_url\": \"https://www.jacars.net\",\n \"target_surface\": \"vehicles\",\n \"app_category\": \"vehicles\",\n \"rate_limit_seconds\": 3,\n \"name\": \"JACars Transfer Taxi\",\n \"list_url\": \"https://www.jacars.net/services/transfer-taxi/\",\n \"app_subcategory\": \"other\",\n \"extractor_config\": {\n \"parser\": \"jacars\",\n \"country\": \"Jamaica\",\n \"crawl_detail_pages\": true,\n \"detail_batch_size\": 10,\n \"require_vehicle_year\": false,\n \"recent_only\": true,\n \"max_age_hours\": 720,\n \"max_listings\": 20\n }\n },\n {\n \"enabled\": true,\n \"base_url\": \"https://www.jacars.net\",\n \"target_surface\": \"vehicles\",\n \"app_category\": \"vehicles\",\n \"rate_limit_seconds\": 3,\n \"name\": \"JACars Moving Services\",\n \"list_url\": \"https://www.jacars.net/services/moving/\",\n \"app_subcategory\": \"other\",\n \"extractor_config\": {\n \"parser\": \"jacars\",\n \"country\": \"Jamaica\",\n \"crawl_detail_pages\": true,\n \"detail_batch_size\": 10,\n \"require_vehicle_year\": false,\n \"recent_only\": true,\n \"max_age_hours\": 720,\n \"max_listings\": 20\n }\n },\n {\n \"enabled\": true,\n \"base_url\": \"https://www.jacars.net\",\n \"target_surface\": \"vehicles\",\n \"app_category\": \"vehicles\",\n \"rate_limit_seconds\": 3,\n \"name\": \"JACars Other Services\",\n \"list_url\": \"https://www.jacars.net/services/other/\",\n \"app_subcategory\": \"other\",\n \"extractor_config\": {\n \"parser\": \"jacars\",\n \"country\": \"Jamaica\",\n \"crawl_detail_pages\": true,\n \"detail_batch_size\": 10,\n \"require_vehicle_year\": false,\n \"recent_only\": true,\n \"max_age_hours\": 720,\n \"max_listings\": 20\n }\n },\n {\n \"enabled\": true,\n \"base_url\": \"https://www.jacars.net\",\n \"target_surface\": \"vehicles\",\n \"app_category\": \"vehicles\",\n \"rate_limit_seconds\": 3,\n \"name\": \"JACars Other Vehicle Listings\",\n \"list_url\": \"https://www.jacars.net/other/\",\n \"app_subcategory\": \"other\",\n \"extractor_config\": {\n \"parser\": \"jacars\",\n \"country\": \"Jamaica\",\n \"crawl_detail_pages\": true,\n \"detail_batch_size\": 10,\n \"require_vehicle_year\": false,\n \"recent_only\": true,\n \"max_age_hours\": 720,\n \"max_listings\": 20\n }\n },\n {\n \"enabled\": true,\n \"base_url\": \"https://www.jacars.net\",\n \"rate_limit_seconds\": 3,\n \"name\": \"JACars Other Real Estate\",\n \"list_url\": \"https://www.jacars.net/other/real-estate/\",\n \"target_surface\": \"marketplace\",\n \"app_category\": \"real_estate\",\n \"app_subcategory\": \"for_sale\",\n \"extractor_config\": {\n \"parser\": \"jacars\",\n \"country\": \"Jamaica\",\n \"crawl_detail_pages\": true,\n \"detail_batch_size\": 10,\n \"require_vehicle_year\": false,\n \"recent_only\": true,\n \"max_age_hours\": 720,\n \"max_listings\": 10\n }\n },\n {\n \"enabled\": true,\n \"base_url\": \"https://www.jacars.net\",\n \"rate_limit_seconds\": 3,\n \"name\": \"JACars Other Jobs\",\n \"list_url\": \"https://www.jacars.net/other/jobs/\",\n \"target_surface\": \"marketplace\",\n \"app_category\": \"jobs\",\n \"app_subcategory\": \"business_office\",\n \"extractor_config\": {\n \"parser\": \"jacars\",\n \"country\": \"Jamaica\",\n \"crawl_detail_pages\": true,\n \"detail_batch_size\": 10,\n \"require_vehicle_year\": false,\n \"recent_only\": true,\n \"max_age_hours\": 720,\n \"max_listings\": 5\n }\n },\n {\n \"enabled\": true,\n \"base_url\": \"https://www.jacars.net\",\n \"rate_limit_seconds\": 3,\n \"name\": \"JACars Other Mobile Phones\",\n \"list_url\": \"https://www.jacars.net/other/telephones/\",\n \"target_surface\": \"marketplace\",\n \"app_category\": \"electronics\",\n \"app_subcategory\": \"phones_accessories\",\n \"extractor_config\": {\n \"parser\": \"jacars\",\n \"country\": \"Jamaica\",\n \"crawl_detail_pages\": true,\n \"detail_batch_size\": 10,\n \"require_vehicle_year\": false,\n \"recent_only\": true,\n \"max_age_hours\": 720,\n \"max_listings\": 10\n }\n },\n {\n \"enabled\": true,\n \"base_url\": \"https://www.jacars.net\",\n \"rate_limit_seconds\": 3,\n \"name\": \"JACars Other Kids Stuff\",\n \"list_url\": \"https://www.jacars.net/other/children-and-babies/\",\n \"target_surface\": \"marketplace\",\n \"app_category\": \"clothing\",\n \"app_subcategory\": \"kids\",\n \"extractor_config\": {\n \"parser\": \"jacars\",\n \"country\": \"Jamaica\",\n \"crawl_detail_pages\": true,\n \"detail_batch_size\": 10,\n \"require_vehicle_year\": false,\n \"recent_only\": true,\n \"max_age_hours\": 720,\n \"max_listings\": 10\n }\n },\n {\n \"enabled\": true,\n \"base_url\": \"https://www.jacars.net\",\n \"rate_limit_seconds\": 3,\n \"name\": \"JACars Other Clothes Footwear Accessories\",\n \"list_url\": \"https://www.jacars.net/other/clothing-shoes-and-accessories/\",\n \"target_surface\": \"marketplace\",\n \"app_category\": \"clothing\",\n \"app_subcategory\": \"accessories\",\n \"extractor_config\": {\n \"parser\": \"jacars\",\n \"country\": \"Jamaica\",\n \"crawl_detail_pages\": true,\n \"detail_batch_size\": 10,\n \"require_vehicle_year\": false,\n \"recent_only\": true,\n \"max_age_hours\": 720,\n \"max_listings\": 10\n }\n },\n {\n \"enabled\": true,\n \"base_url\": \"https://www.jacars.net\",\n \"rate_limit_seconds\": 3,\n \"name\": \"JACars Other Computers Peripherals\",\n \"list_url\": \"https://www.jacars.net/other/computers-and-games/\",\n \"target_surface\": \"marketplace\",\n \"app_category\": \"electronics\",\n \"app_subcategory\": \"computers_tablets\",\n \"extractor_config\": {\n \"parser\": \"jacars\",\n \"country\": \"Jamaica\",\n \"crawl_detail_pages\": true,\n \"detail_batch_size\": 10,\n \"require_vehicle_year\": false,\n \"recent_only\": true,\n \"max_age_hours\": 720,\n \"max_listings\": 10\n }\n },\n {\n \"enabled\": true,\n \"base_url\": \"https://www.jacars.net\",\n \"rate_limit_seconds\": 3,\n \"name\": \"JACars Other Electronics Appliances\",\n \"list_url\": \"https://www.jacars.net/other/electronics-and-appliances/\",\n \"target_surface\": \"marketplace\",\n \"app_category\": \"electronics\",\n \"app_subcategory\": \"other\",\n \"extractor_config\": {\n \"parser\": \"jacars\",\n \"country\": \"Jamaica\",\n \"crawl_detail_pages\": true,\n \"detail_batch_size\": 10,\n \"require_vehicle_year\": false,\n \"recent_only\": true,\n \"max_age_hours\": 720,\n \"max_listings\": 10\n }\n },\n {\n \"enabled\": true,\n \"base_url\": \"https://www.jacars.net\",\n \"rate_limit_seconds\": 3,\n \"name\": \"JACars Other Home Garden Pool\",\n \"list_url\": \"https://www.jacars.net/other/home-gardens-and-pools/\",\n \"target_surface\": \"marketplace\",\n \"app_category\": \"buy_sell\",\n \"app_subcategory\": \"home_garden\",\n \"extractor_config\": {\n \"parser\": \"jacars\",\n \"country\": \"Jamaica\",\n \"crawl_detail_pages\": true,\n \"detail_batch_size\": 10,\n \"require_vehicle_year\": false,\n \"recent_only\": true,\n \"max_age_hours\": 720,\n \"max_listings\": 10\n }\n },\n {\n \"enabled\": true,\n \"base_url\": \"https://www.jacars.net\",\n \"rate_limit_seconds\": 3,\n \"name\": \"JACars Other Hobbies Sports Leisure\",\n \"list_url\": \"https://www.jacars.net/other/hobbies-sports-leisure-and-travel/\",\n \"target_surface\": \"marketplace\",\n \"app_category\": \"buy_sell\",\n \"app_subcategory\": \"hobbies_sports\",\n \"extractor_config\": {\n \"parser\": \"jacars\",\n \"country\": \"Jamaica\",\n \"crawl_detail_pages\": true,\n \"detail_batch_size\": 10,\n \"require_vehicle_year\": false,\n \"recent_only\": true,\n \"max_age_hours\": 720,\n \"max_listings\": 10\n }\n },\n {\n \"enabled\": true,\n \"base_url\": \"https://www.jacars.net\",\n \"rate_limit_seconds\": 3,\n \"name\": \"JACars Other Animals Pets\",\n \"list_url\": \"https://www.jacars.net/other/animals/\",\n \"target_surface\": \"marketplace\",\n \"app_category\": \"other\",\n \"app_subcategory\": \"pets\",\n \"extractor_config\": {\n \"parser\": \"jacars\",\n \"country\": \"Jamaica\",\n \"crawl_detail_pages\": true,\n \"detail_batch_size\": 10,\n \"require_vehicle_year\": false,\n \"recent_only\": true,\n \"max_age_hours\": 720,\n \"max_listings\": 10\n }\n },\n {\n \"enabled\": true,\n \"base_url\": \"https://www.jacars.net\",\n \"rate_limit_seconds\": 3,\n \"name\": \"JACars Other Health Beauty\",\n \"list_url\": \"https://www.jacars.net/other/beauty-and-health/\",\n \"target_surface\": \"marketplace\",\n \"app_category\": \"services\",\n \"app_subcategory\": \"health_beauty\",\n \"extractor_config\": {\n \"parser\": \"jacars\",\n \"country\": \"Jamaica\",\n \"crawl_detail_pages\": true,\n \"detail_batch_size\": 10,\n \"require_vehicle_year\": false,\n \"recent_only\": true,\n \"max_age_hours\": 720,\n \"max_listings\": 10\n }\n },\n {\n \"enabled\": true,\n \"base_url\": \"https://www.jacars.net\",\n \"rate_limit_seconds\": 3,\n \"name\": \"JACars Other Business\",\n \"list_url\": \"https://www.jacars.net/other/business/\",\n \"target_surface\": \"marketplace\",\n \"app_category\": \"buy_sell\",\n \"app_subcategory\": \"business\",\n \"extractor_config\": {\n \"parser\": \"jacars\",\n \"country\": \"Jamaica\",\n \"crawl_detail_pages\": true,\n \"detail_batch_size\": 10,\n \"require_vehicle_year\": false,\n \"recent_only\": true,\n \"max_age_hours\": 720,\n \"max_listings\": 10\n }\n },\n {\n \"enabled\": true,\n \"base_url\": \"https://www.jacars.net\",\n \"rate_limit_seconds\": 3,\n \"name\": \"JACars Other General Services\",\n \"list_url\": \"https://www.jacars.net/other/services/\",\n \"target_surface\": \"marketplace\",\n \"app_category\": \"services\",\n \"app_subcategory\": \"other\",\n \"extractor_config\": {\n \"parser\": \"jacars\",\n \"country\": \"Jamaica\",\n \"crawl_detail_pages\": true,\n \"detail_batch_size\": 10,\n \"require_vehicle_year\": false,\n \"recent_only\": true,\n \"max_age_hours\": 720,\n \"max_listings\": 10\n }\n }\n]",
"type": "string"
}
]
},
"options": {}
},
"id": "set-config",
"name": "Set Config Here",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
420,
320
]
},
{
"parameters": {
"method": "POST",
"url": "={{ $json.crawl4aiUrl }}",
"sendBody": true,
"contentType": "json",
"specifyBody": "json",
"jsonBody": "={{ { urls: JSON.parse($json.sourcesJson).filter(source => source.enabled !== false).map(source => source.list_url), browser_config: { type: \"BrowserConfig\", params: { headless: true } }, crawler_config: { type: \"CrawlerRunConfig\", params: { stream: false, cache_mode: \"bypass\" } } } }}",
"options": {}
},
"id": "crawl4ai-http",
"name": "Crawl4AI",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
720,
320
]
},
{
"parameters": {
"mode": "combine",
"combineBy": "combineByPosition",
"options": {}
},
"id": "merge-config-crawl",
"name": "Merge Config + Crawl Result",
"type": "n8n-nodes-base.merge",
"typeVersion": 3,
"position": [
1020,
320
]
},
{
"parameters": {
"jsCode": "const merged = $input.first().json;\nconst input = { ...merged, crawl4aiResult: merged };\nconst OWNER = input.githubOwner || 'bisco401';\nconst REPO = input.githubRepo || '6ixo';\nconst BRANCH = input.githubBranch || 'main';\nconst CSV_PATH = input.csvPath || 'data/scraped-listings.csv';\nconst TOKEN = input.githubToken;\nconst DEFAULT_STATUS = input.defaultImportStatus || 'pending';\nlet SOURCES = [];\ntry {\n SOURCES = typeof input.sourcesJson === 'string' ? JSON.parse(input.sourcesJson || '[]') : input.sourcesJson;\n} catch (error) {\n throw new Error('sourcesJson is not valid JSON. Do not edit it unless you need to add more sources.');\n}\n\nif (!TOKEN || TOKEN === 'PASTE_GITHUB_TOKEN_HERE') throw new Error('Paste your GitHub token into the Set Config Here node.');\nif (!Array.isArray(SOURCES) || !SOURCES.length) throw new Error('The Set Config Here node needs at least one source in sourcesJson.');\n\nconst httpRequest = async (options) => {\n const request = {\n method: options.method || 'GET',\n uri: options.url,\n url: options.url,\n headers: options.headers || {},\n body: options.body,\n json: options.json === true,\n resolveWithFullResponse: true,\n simple: false\n };\n try {\n const response = await this.helpers.httpRequest(request);\n const status = response.statusCode || response.status || 200;\n const body = response.body ?? response;\n return { status, ok: status >= 200 && status < 300, body };\n } catch (error) {\n const status = error.statusCode || error.status || error.response?.status || error.response?.statusCode || 500;\n const body = error.response?.body || error.response?.data || error.message || '';\n return { status, ok: status >= 200 && status < 300, body };\n }\n};\n\nconst csvHeaders = ['id','status','target_surface','app_category','app_subcategory','title','price_text','price_value','currency','city','country','seller','phone','description','image_urls','source_site','source_url','scraped_at','make','model','trim','year','condition','transmission','color','mileage_km','attributes'];\nconst ghHeaders = { authorization: `Bearer ${TOKEN}`, accept: 'application/vnd.github+json', 'x-github-api-version': '2022-11-28' };\nconst sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\nconst strip = (value = '') => String(value || '').replace(/<script[\\s\\S]*?<\\/script>/gi, ' ').replace(/<style[\\s\\S]*?<\\/style>/gi, ' ').replace(/<[^>]+>/g, ' ').replace(/ /g, ' ').replace(/&/g, '&').replace(/₵|₵/gi, 'GH\u00a2').replace(/\\s+/g, ' ').trim();\nconst rx = (pattern, flags = 'i') => pattern ? new RegExp(pattern, flags) : null;\nconst firstMatch = (text, pattern, flags = 'i') => {\n const re = rx(pattern, flags);\n if (!re) return '';\n const match = String(text || '').match(re);\n return strip(match?.[1] || match?.[0] || '');\n};\nconst absUrl = (value, base) => {\n try { return new URL(String(value || ''), base || undefined).toString(); }\n catch { return String(value || '').trim(); }\n};\nconst priceValue = (priceText = '') => {\n const parsed = Number(String(priceText || '').replace(/[^0-9.]/g, ''));\n return Number.isFinite(parsed) ? String(parsed) : '';\n};\nconst currency = (priceText = '') => {\n const text = String(priceText || '');\n if (/JA\\$|JMD/i.test(text)) return 'JMD';\n if (/GH\u00a2|GHS/i.test(text)) return 'GHS';\n if (/\\$/.test(text)) return 'USD';\n if (/\u00a3/.test(text)) return 'GBP';\n if (/\u20ac/.test(text)) return 'EUR';\n return '';\n};\nconst guessYear = (text = '') => String(String(text || '').match(/\\b(19|20)\\d{2}\\b/)?.[0] || '');\nconst parseJacarsSourceDate = (value = '', now = new Date()) => {\n const text = normalizeWhitespace(value).toLowerCase();\n if (!text) return '';\n const posted = new Date(now);\n const amount = Number(text.match(/\\d+/)?.[0] || 0);\n if (/just now|today/.test(text)) return posted.toISOString();\n if (/minute/.test(text)) posted.setMinutes(posted.getMinutes() - amount);\n else if (/hour/.test(text)) posted.setHours(posted.getHours() - amount);\n else if (/yesterday/.test(text)) { posted.setDate(posted.getDate() - 1); posted.setHours(12, 0, 0, 0); }\n else if (/day/.test(text)) posted.setDate(posted.getDate() - amount);\n else if (/week/.test(text)) posted.setDate(posted.getDate() - (amount * 7));\n else if (/month/.test(text)) posted.setMonth(posted.getMonth() - amount);\n else return '';\n return posted.toISOString();\n};\nconst sortNewestFirst = (rows = []) => rows.slice().sort((a, b) => {\n const aTime = Date.parse(a.scraped_at || '');\n const bTime = Date.parse(b.scraped_at || '');\n return (Number.isFinite(bTime) ? bTime : 0) - (Number.isFinite(aTime) ? aTime : 0);\n});\nconst normalizeCondition = (value = '') => {\n const text = String(value || '').toLowerCase();\n if (/like new/.test(text)) return 'like_new';\n if (/excellent/.test(text)) return 'excellent';\n if (/good/.test(text)) return 'good';\n if (/brand new|\\bnew\\b/.test(text)) return 'new';\n if (/foreign used|local used|used|pre-owned|preowned/.test(text)) return 'used';\n return '';\n};\nconst csvEscape = (value = '') => {\n const text = String(value ?? '');\n return /[\",\\n\\r]/.test(text) ? `\"${text.replace(/\"/g, '\"\"')}\"` : text;\n};\nconst parseCsv = (text = '') => {\n const rows = [];\n let row = [];\n let cell = '';\n let quoted = false;\n const pushCell = () => { row.push(cell); cell = ''; };\n const pushRow = () => { pushCell(); if (row.some((v) => String(v || '').trim())) rows.push(row); row = []; };\n const input = String(text || '').replace(/^\\uFEFF/, '');\n for (let i = 0; i < input.length; i += 1) {\n const char = input[i];\n const next = input[i + 1];\n if (quoted) {\n if (char === '\"' && next === '\"') { cell += '\"'; i += 1; }\n else if (char === '\"') quoted = false;\n else cell += char;\n } else if (char === '\"') quoted = true;\n else if (char === ',') pushCell();\n else if (char === '\\n') pushRow();\n else if (char !== '\\r') cell += char;\n }\n if (cell || row.length) pushRow();\n const headers = (rows.shift() || csvHeaders).map((h) => String(h || '').trim());\n return rows.map((values) => headers.reduce((acc, header, index) => { if (header) acc[header] = values[index] || ''; return acc; }, {}));\n};\nconst toCsv = (rows = []) => [csvHeaders.join(','), ...rows.map((row) => csvHeaders.map((header) => csvEscape(row[header] || '')).join(','))].join('\\n') + '\\n';\nconst getGithubCsv = async () => {\n const url = `https://api.github.com/repos/${OWNER}/${REPO}/contents/${encodeURIComponent(CSV_PATH).replace(/%2F/g, '/')}?ref=${BRANCH}`;\n const res = await httpRequest({ url, headers: ghHeaders, json: true });\n if (res.status === 404) return { sha: null, text: csvHeaders.join(',') + '\\n' };\n if (!res.ok) throw new Error(`Could not read ${CSV_PATH}: ${res.status} ${typeof res.body === 'string' ? res.body : JSON.stringify(res.body)}`);\n const json = typeof res.body === 'string' ? JSON.parse(res.body) : res.body;\n return { sha: json.sha, text: Buffer.from(json.content || '', 'base64').toString('utf8') };\n};\nconst putGithubCsv = async (text, sha) => {\n const url = `https://api.github.com/repos/${OWNER}/${REPO}/contents/${encodeURIComponent(CSV_PATH).replace(/%2F/g, '/')}`;\n const body = {\n message: 'Update scraped listings CSV',\n branch: BRANCH,\n content: Buffer.from(text, 'utf8').toString('base64'),\n ...(sha ? { sha } : {})\n };\n const res = await httpRequest({ url, method: 'PUT', headers: { ...ghHeaders, 'content-type': 'application/json' }, body, json: true });\n if (!res.ok) throw new Error(`Could not update ${CSV_PATH}: ${res.status} ${typeof res.body === 'string' ? res.body : JSON.stringify(res.body)}`);\n return typeof res.body === 'string' ? JSON.parse(res.body) : res.body;\n};\nconst normalizeWhitespace = (value = '') => strip(String(value || '').replace(/\\s+/g, ' '));\nconst decodeHtml = (value = '') => String(value || '')\n .replace(/"/g, '\"')\n .replace(/"/g, '\"')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(///g, '/');\nconst extractAttr = (text = '', attr = 'href') => {\n const match = String(text || '').match(new RegExp(attr + \"=[\\\"']([^\\\"']+)[\\\"']\", 'i'));\n return decodeHtml(match?.[1] || '');\n};\nconst splitMakeModel = (title = '') => {\n const cleaned = normalizeWhitespace(title).replace(/\\b(19|20)\\d{2}\\b.*$/, '').replace(/\\b\\d+[,.]\\d+L\\b/i, '').trim();\n const multi = ['Mercedes-Benz', 'Land Rover', 'Rolls Royce', 'Alfa Romeo', 'Aston Martin'];\n const found = multi.find((make) => cleaned.toLowerCase().startsWith(make.toLowerCase()));\n if (found) return { make: found, model: cleaned.slice(found.length).trim() };\n const [make = '', ...rest] = cleaned.split(/\\s+/);\n return { make, model: rest.join(' ') };\n};\nconst extractPhoneFromJacarsDetail = (html = '') => {\n const text = String(html || '');\n const whatsapp = text.match(/api\\.whatsapp\\.com\\/send\\?phone=([^&\"']+)/i);\n if (whatsapp?.[1]) return decodeURIComponent(whatsapp[1]).replace(/\\s+/g, '').trim();\n const tel = text.match(/href=[\"']tel:([^\"']+)[\"']/i);\n if (tel?.[1] && !/\\{\\{/.test(tel[1])) return decodeURIComponent(tel[1]).replace(/\\s+/g, '').trim();\n const visible = text.match(/phone-author-subtext__main[^>]*>([^<]+)<[\\s\\S]{0,140}?phone-author-subtext__mask[^>]*>([^<]*)</i);\n if (visible?.[1]) return strip(String(visible[1] || '') + String(visible[2] || '')).replace(/\\s+/g, '').trim();\n return '';\n};\nconst extractJacarsDetailFields = (html = '') => {\n const text = String(html || '');\n const field = (label) => normalizeWhitespace(firstMatch(text, '<li[^>]*>\\\\s*\\\\*?\\\\s*' + label + ':\\\\s*([\\\\s\\\\S]*?)<\\\\/li>', 'i'));\n const markdownField = (label) => normalizeWhitespace(firstMatch(text, '\\\\*\\\\s*' + label + ':\\\\s*([^\\\\n]+)', 'i'));\n return {\n phone: extractPhoneFromJacarsDetail(text),\n color: field('Color') || markdownField('Color'),\n transmission: field('Gearbox') || markdownField('Gearbox'),\n mileage_km: (field('Mileage') || markdownField('Mileage')).replace(/[^0-9]/g, ''),\n description: normalizeWhitespace(firstMatch(text, \"<div[^>]+class=[\\\"'][^\\\"']*announcement-description[^\\\"']*[\\\"'][^>]*>([\\\\s\\\\S]*?)<\\\\/div>\", 'i'))\n };\n};\nconst crawlDetailPages = async (urls = []) => {\n const endpoint = input.crawl4aiUrl;\n if (!endpoint || !urls.length) return [];\n const res = await httpRequest({\n url: endpoint,\n method: 'POST',\n body: {\n urls,\n browser_config: { type: 'BrowserConfig', params: { headless: true } },\n crawler_config: { type: 'CrawlerRunConfig', params: { stream: false, cache_mode: 'bypass' } }\n },\n json: true\n });\n if (!res.ok) throw new Error('Crawl4AI detail crawl failed: ' + res.status + ' ' + (typeof res.body === 'string' ? res.body : JSON.stringify(res.body)));\n return normalizeCrawlItems(res.body);\n};\n\nconst extractJacarsListings = (source, html) => {\n const config = source.extractor_config || {};\n const input = String(html || '');\n const starts = [];\n const startRe = /<div[^>]+class=[\"'][^\"']*\\badvert(?:-grid)?\\b[^\"']*(?:\\bjs-item-listing\\b|\\bjs-advert-click\\b)[^\"']*[\"'][^>]*>/gi;\n let match;\n while ((match = startRe.exec(input))) starts.push(match.index);\n const cards = starts.map((start, index) => input.slice(start, starts[index + 1] || input.length));\n const rows = [];\n const seen = new Set();\n const targetSurface = source.target_surface || 'vehicles';\n const appCategory = source.app_category || 'vehicles';\n const appSubcategory = source.app_subcategory || 'vehicles';\n const vehicleCoreListing = appCategory === 'vehicles' && ['vehicles', 'cars'].includes(String(appSubcategory || '').toLowerCase());\n const requireVehicleYear = config.require_vehicle_year === true || (config.require_vehicle_year !== false && vehicleCoreListing);\n for (const card of cards) {\n const id = firstMatch(card, \"data-id=[\\\"']?(\\\\d+)[\\\"']?\", 'i') || firstMatch(card, \"id=[\\\"']?(\\\\d+)[\\\"']?\", 'i');\n const titleLink = card.match(/<a[^>]+class=[\"'][^\"']*advert(?:-grid)?__content-title[^\"']*[\"'][\\s\\S]*?<\\/a>/i)?.[0] || '';\n const title = normalizeWhitespace(titleLink || firstMatch(card, 'advert(?:-grid)?__content-title[\\\\s\\\\S]*?>([\\\\s\\\\S]*?)<\\\\/a>', 'i'));\n if (!title) continue;\n if (requireVehicleYear && !/\\b(19|20)\\d{2}\\b/.test(title)) continue;\n const href = extractAttr(titleLink, 'href') || firstMatch(card, \"<a[^>]+class=[\\\"'][^\\\"']*mask[^\\\"']*[\\\"'][^>]+href=[\\\"']([^\\\"']+)\", 'i') || (id ? '/adv/' + id + '/' : '');\n const sourceUrl = absUrl(href || source.list_url, source.base_url || source.list_url);\n if (seen.has(sourceUrl)) continue;\n seen.add(sourceUrl);\n const priceBlock = card.match(/<a[^>]+class=[\"'][^\"']*advert(?:-grid)?__content-price[^\"']*[\"'][\\s\\S]*?<\\/a>/i)?.[0] || '';\n let priceText = normalizeWhitespace(priceBlock).replace(/JA\\$\\s*/i, 'JA$ ');\n if (!priceText) priceText = firstMatch(card, '(JA\\\\$\\\\s?[0-9,.]+(?:\\\\s?JA\\\\$\\\\s?[0-9,.]+)?|Call for price)', 'i');\n const place = normalizeWhitespace(firstMatch(card, \"<(?:div|span)[^>]+class=[\\\"'][^\\\"']*advert(?:-grid)?__content-place[^\\\"']*[\\\"'][^>]*>([\\\\s\\\\S]*?)<\\\\/(?:div|span)>\", 'i'));\n const dateText = normalizeWhitespace(firstMatch(card, \"<(?:div|span)[^>]+class=[\\\"'][^\\\"']*advert(?:-grid)?__content-date[^\\\"']*[\\\"'][^>]*>([\\\\s\\\\S]*?)<\\\\/(?:div|span)>\", 'i'));\n const sourcePostedAt = parseJacarsSourceDate(dateText) || new Date().toISOString();\n const seller = normalizeWhitespace(firstMatch(card, 'advert__header-name[\\\\s\\\\S]*?<span>([\\\\s\\\\S]*?)<\\\\/span>', 'i')) || source.name || '';\n const imageMatches = [\n ...card.matchAll(/data-background=[\"']([^\"']+)[\"']/gi),\n ...card.matchAll(/background-image:\\s*url\\((?:"|[\"']?)(.*?)(?:"|[\"']?)\\)/gi),\n ...card.matchAll(/<img[^>]+src=[\"']([^\"']+)[\"']/gi)\n ].map((m) => decodeHtml(m[1])).filter(Boolean).map((url) => absUrl(url, source.base_url || source.list_url));\n const images = [...new Set(imageMatches)].join('|');\n const year = guessYear(title);\n const { make, model } = splitMakeModel(title);\n rows.push({\n id: id ? 'jacars-' + id : 'csv-' + Math.abs([...sourceUrl].reduce((a, c) => ((a << 5) - a + c.charCodeAt(0)) | 0, 0)),\n status: source.default_status || DEFAULT_STATUS,\n target_surface: targetSurface,\n app_category: appCategory,\n app_subcategory: appSubcategory,\n title,\n price_text: priceText,\n price_value: priceValue(priceText),\n currency: currency(priceText) || 'JMD',\n city: place,\n country: config.country || 'Jamaica',\n seller,\n phone: '',\n description: [title, priceText, place, dateText].filter(Boolean).join(' - '),\n image_urls: images,\n source_site: source.name || 'JACars',\n source_url: sourceUrl,\n scraped_at: sourcePostedAt,\n make: vehicleCoreListing ? make : '',\n model: vehicleCoreListing ? model : '',\n trim: '',\n year,\n condition: config.condition || (vehicleCoreListing ? 'used' : normalizeCondition([title, priceText, place, dateText].filter(Boolean).join(' ')) || 'good'),\n transmission: '',\n color: '',\n mileage_km: '',\n attributes: JSON.stringify({ sourceDate: dateText, sourcePostedAt, parser: 'jacars', sourceCategory: source.name || '', appSubcategory })\n });\n }\n let nextRows = sortNewestFirst(rows);\n const maxAgeHours = Number(config.max_age_hours || 0);\n if (config.recent_only !== false && maxAgeHours > 0) {\n const cutoff = Date.now() - (maxAgeHours * 60 * 60 * 1000);\n nextRows = nextRows.filter((row) => {\n const posted = Date.parse(row.scraped_at || '');\n return Number.isFinite(posted) && posted >= cutoff;\n });\n }\n const maxListings = Number(config.max_listings || 0);\n if (maxListings > 0) nextRows = nextRows.slice(0, maxListings);\n return nextRows;\n};\n\nconst extractListings = (source, html) => {\n const config = source.extractor_config || {};\n if (String(config.parser || '').toLowerCase() === 'jacars' || /jacars/i.test(source.name || '')) return extractJacarsListings(source, html);\n const base = source.base_url || source.list_url;\n const cardRe = rx(config.cardPattern, 'gi');\n const cards = cardRe ? Array.from(String(html || '').matchAll(cardRe)).map((m) => m[0]) : [html];\n return cards.map((card) => {\n const title = firstMatch(card, config.titlePattern || '<h[12][^>]*>([\\\\s\\\\S]*?)<\\\\/h[12]>', 'i') || firstMatch(card, config.fallbackTitlePattern || 'title=\"([^\"]+)\"', 'i');\n const href = firstMatch(card, config.urlPattern || 'href=\"([^\"]+)\"', 'i');\n const sourceUrl = absUrl(href || source.list_url, base);\n const priceText = firstMatch(card, config.pricePattern || '(GH\u00a2|GHS|\\\\$|\u00a3|\u20ac)\\\\s?[0-9,.]+', 'i');\n const image = firstMatch(card, config.imagePattern || '<img[^>]+src=\"([^\"]+)\"', 'i');\n const location = firstMatch(card, config.locationPattern || '', 'i');\n const description = firstMatch(card, config.descriptionPattern || '', 'i') || strip(card).slice(0, 280);\n const attributes = {\n employmentType: config.employmentType || '',\n experienceLevel: config.experienceLevel || '',\n remote: Boolean(config.remote),\n tags: Array.isArray(config.tags) ? config.tags : []\n };\n const row = {\n id: sourceUrl ? `csv-${Math.abs([...sourceUrl].reduce((a, c) => ((a << 5) - a + c.charCodeAt(0)) | 0, 0))}` : `csv-${Date.now()}`,\n status: source.default_status || DEFAULT_STATUS,\n target_surface: source.target_surface || (source.app_category === 'vehicles' ? 'vehicles' : 'marketplace'),\n app_category: source.app_category || 'electronics',\n app_subcategory: source.app_subcategory || '',\n title,\n price_text: priceText,\n price_value: priceValue(priceText),\n currency: currency(priceText),\n city: config.city || location.split(',')[0]?.trim() || '',\n country: config.country || '',\n seller: firstMatch(card, config.sellerPattern || '', 'i') || config.seller || source.name,\n phone: firstMatch(card, config.phonePattern || '(\\\\+?\\\\d[\\\\d\\\\s().-]{7,}\\\\d)', 'i') || config.phone || '',\n description,\n image_urls: image ? absUrl(image, base) : '',\n source_site: source.name || '',\n source_url: sourceUrl,\n scraped_at: new Date().toISOString(),\n make: firstMatch(card, config.makePattern || '', 'i') || config.make || '',\n model: firstMatch(card, config.modelPattern || '', 'i') || config.model || '',\n trim: firstMatch(card, config.trimPattern || '', 'i') || config.trim || '',\n year: firstMatch(card, config.yearPattern || '', 'i') || guessYear(`${title} ${description}`),\n condition: firstMatch(card, config.conditionPattern || '', 'i') || normalizeCondition(`${title} ${description}`),\n transmission: firstMatch(card, config.transmissionPattern || '(Automatic|Manual)', 'i'),\n color: firstMatch(card, config.colorPattern || '', 'i') || config.color || '',\n mileage_km: firstMatch(card, config.mileagePattern || '', 'i'),\n attributes: JSON.stringify(attributes)\n };\n return row;\n }).filter((row) => row.title && row.source_url);\n};\n\nconst normalizeCrawlItems = (value) => {\n if (!value) return [];\n if (Array.isArray(value)) return value.flatMap(normalizeCrawlItems);\n if (Array.isArray(value.results)) return value.results;\n if (Array.isArray(value.data)) return value.data;\n if (value.body) return normalizeCrawlItems(value.body);\n return [value];\n};\nconst crawlItems = normalizeCrawlItems(input.crawl4aiResult || input);\nconst crawlByUrl = new Map();\nfor (const item of crawlItems) {\n const url = String(item.url || item.redirected_url || '').trim();\n const text = item.html || item.cleaned_html || item.fit_html || item.markdown?.raw_markdown || item.markdown || item.fit_markdown || '';\n if (url) crawlByUrl.set(url, text);\n if (!url && text) crawlByUrl.set('__fallback__', text);\n}\n\nconst existing = await getGithubCsv();\nconst rowsByUrl = new Map(parseCsv(existing.text).map((row) => [String(row.source_url || '').trim(), row]));\nconst summary = [];\nfor (const source of SOURCES.filter((item) => item && item.enabled !== false)) {\n let fetched = 0;\n let inserted = 0;\n try {\n const html = crawlByUrl.get(source.list_url) || crawlByUrl.get(absUrl(source.list_url, source.base_url)) || crawlByUrl.get('__fallback__') || '';\n if (!html) throw new Error('Crawl4AI did not return html/markdown for this source URL.');\n if (/Just a moment|cf-mitigated|challenges.cloudflare.com/i.test(html)) throw new Error('Blocked by anti-bot challenge. Use an allowed source, API, export, saved HTML, or screenshot/OCR for this source.');\n let scraped = extractListings(source, html);\n if ((source.extractor_config || {}).crawl_detail_pages && scraped.length) {\n const batchSize = Math.max(1, Number((source.extractor_config || {}).detail_batch_size || 10));\n const detailByUrl = new Map();\n for (let i = 0; i < scraped.length; i += batchSize) {\n const batchUrls = scraped.slice(i, i + batchSize).map((row) => row.source_url).filter(Boolean);\n const detailItems = await crawlDetailPages(batchUrls);\n for (const detail of detailItems) {\n const url = String(detail.url || detail.redirected_url || '').trim();\n const detailHtml = detail.html || detail.cleaned_html || detail.fit_html || detail.markdown?.raw_markdown || detail.markdown || '';\n if (url && detailHtml) detailByUrl.set(url, extractJacarsDetailFields(detailHtml));\n }\n }\n scraped = scraped.map((row) => {\n const detail = detailByUrl.get(row.source_url) || {};\n return {\n ...row,\n phone: detail.phone || row.phone || '',\n color: detail.color || row.color || '',\n transmission: detail.transmission || row.transmission || '',\n mileage_km: detail.mileage_km || row.mileage_km || '',\n description: detail.description || row.description || ''\n };\n });\n }\n fetched = scraped.length;\n for (const row of scraped) {\n const key = String(row.source_url || '').trim();\n if (!key) continue;\n rowsByUrl.set(key, { ...(rowsByUrl.get(key) || {}), ...row });\n inserted += 1;\n }\n summary.push({ source: source.name, status: 'success', fetched, inserted, mode: 'crawl4ai' });\n } catch (error) {\n summary.push({ source: source.name, status: 'failed', fetched, inserted, mode: 'crawl4ai', error: error.message });\n }\n}\nconst nextRows = Array.from(rowsByUrl.values()).sort((a, b) => String(b.scraped_at || '').localeCompare(String(a.scraped_at || '')));\nawait putGithubCsv(toCsv(nextRows), existing.sha);\nreturn summary.map((row) => ({ json: row }));"
},
"id": "scrape-to-csv",
"name": "Scrape + Update CSV",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1320,
320
]
}
],
"connections": {
"Manual Trigger": {
"main": [
[
{
"node": "Set Config Here",
"type": "main",
"index": 0
}
]
]
},
"Every 6 Hours": {
"main": [
[
{
"node": "Set Config Here",
"type": "main",
"index": 0
}
]
]
},
"Set Config Here": {
"main": [
[
{
"node": "Crawl4AI",
"type": "main",
"index": 0
},
{
"node": "Merge Config + Crawl Result",
"type": "main",
"index": 0
}
]
]
},
"Crawl4AI": {
"main": [
[
{
"node": "Merge Config + Crawl Result",
"type": "main",
"index": 1
}
]
]
},
"Merge Config + Crawl Result": {
"main": [
[
{
"node": "Scrape + Update CSV",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
},
"staticData": null,
"tags": [
{
"name": "6ixo"
}
],
"triggerCount": 0,
"updatedAt": "2026-05-05T20:06:58.885Z",
"versionId": "6ixo-crawl4ai-csv"
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
6ixo - Crawl4AI Listings to CSV. Event-driven trigger; 4 nodes.
Source: https://github.com/bisco401/6ixo/blob/2cd92f544b47908f6f49077cd6506f0538e22259/automations/n8n/6ixo-crawl4ai-to-csv-github.json — 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.
Workflow 01.01. Uses notion, executeWorkflowTrigger, httpRequest. Event-driven trigger; 60 nodes.
Lmchatopenai Workflow. Uses noOp, stickyNote, executeWorkflowTrigger, airtable. Event-driven trigger; 41 nodes.
This n8n workflow retrieves an Airtable record along with its related child records in a hierarchical structure. It can fetch up to 3 levels of linked records and assembles them into a comprehensive J
Automate sales call analysis and store structured insights in Notion with AI-powered intelligence.
This workflow allows you to batch update/insert Airtable rows in groups of 10, significantly reducing the number of API calls and increasing performance. Copy the 3 Nodes Copy the three nodes inside t