This workflow corresponds to n8n.io template #13781 — we link there as the canonical source.
This workflow follows the Chainllm → 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 →
{
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Train and deploy ML models with Claude AI and Slack approval",
"tags": [],
"nodes": [
{
"id": "874019bc-a6b2-4fa5-8468-77266a7be43f",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
5520,
928
],
"parameters": {
"width": 1100,
"height": 1304,
"content": "## \ud83e\udd16 Autonomous MLOps Pipeline\n\n**Trigger a full end-to-end machine learning cycle from a single webhook call.**\n\nClaude AI plans the strategy, cleans your data, engineers features, trains 3 models head-to-head, picks the winner, writes a model card \u2014 then asks a human on Slack before deploying to GitHub.\n\n---\n\n### \ud83d\udd01 How It Works\n\n| Phase | What Happens |\n|-------|-------------|\n| **P1 \u2014 Strategy** | Claude Sonnet reads your dataset URL + goal, outputs a JSON ML plan (features, algorithms, metric) |\n| **P2 \u2014 Data Engineering** | Fetches the CSV, handles quoted fields, drops nulls, encodes Sex/Embarked, imputes missing Age |\n| **P3 \u2014 Feature Engineering** | Claude Haiku approves 3 features; code engineers FamilySize, IsAlone, TitleEncoded |\n| **P4 \u2014 Train & Evaluate** | Logistic Regression, Random Forest, and XGBoost trained from scratch in JS; Claude Sonnet judges the winner by F1 score |\n| **P5 \u2014 HITL Deployment** | Claude writes a MODEL_CARD.md; Slack message asks human to approve before GitHub push |\n\n---\n\n### \u2699\ufe0f Setup (5 minutes)\n\n**Credentials needed:**\n- `Anthropic API` \u2014 API key from console.anthropic.com\n- `Slack Bot Token` \u2014 OAuth bot token with `chat:write` scope\n\n**Supabase audit log (optional):**\nReplace `YOUR_SUPABASE_SERVICE_ROLE_KEY` and `YOUR_PROJECT.supabase.co` in the 5 log nodes, or delete them if you don't need audit logging. Table schema:\n```sql\ncreate table mlops_audit_log (\n id bigint generated always as identity primary key,\n run_id text, workflow_step text, phase text, status text,\n created_at timestamptz default now()\n);\n```\n\n**Invite your Slack bot:**\nIn the target channel, run `/invite @your-bot-name` before the first run.\n\n---\n\n### \ud83d\ude80 Trigger\n\n```bash\nPOST /webhook/mlops-v2\nContent-Type: application/json\n\n{\n \"dataset_url\": \"https://your-host.com/data.csv\",\n \"target_variable\": \"Survived\",\n \"business_goal\": \"Predict passenger survival to optimise boarding policy\"\n}\n```\n\n**Tested on:** Titanic dataset \u2192 **XGBoost wins** with F1=0.761, Accuracy=0.821"
},
"typeVersion": 1
},
{
"id": "2b566773-44cd-4255-8a83-8949ff44ad18",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
6800,
1408
],
"parameters": {
"color": 3,
"width": 956,
"height": 80,
"content": "### Phase 1 \u2014 Orchestration & Strategy\n`Webhook` \u2192 `Claude Sonnet` plans ML strategy \u2192 `Supabase` audit log"
},
"typeVersion": 1
},
{
"id": "7fcb4e3a-f171-406f-9570-6f98870a891e",
"name": "P1: Receive MLOps Job",
"type": "n8n-nodes-base.webhook",
"position": [
6864,
1616
],
"parameters": {
"path": "mlops-v2",
"options": {},
"httpMethod": "POST"
},
"typeVersion": 2
},
{
"id": "af15b85b-a1b1-45e5-a228-bbcd0267bb8d",
"name": "P1: Plan ML Strategy",
"type": "@n8n/n8n-nodes-langchain.chainLlm",
"position": [
7072,
1616
],
"parameters": {
"text": "=You are a Lead Data Scientist. Given this ML job, return a JSON plan (no markdown, raw JSON only).\n\nDataset URL: {{ $json.body.dataset_url }}\nTarget Variable: {{ $json.body.target_variable }}\nBusiness Goal: {{ $json.body.business_goal }}\n\nReturn exactly this JSON structure:\n{\n \"dataset_url\": \"<same url>\",\n \"target_variable\": \"<same target>\",\n \"business_goal\": \"<same goal>\",\n \"cleaning_steps\": [\"drop nulls\", \"encode categoricals\"],\n \"feature_ideas\": [\"FamilySize\", \"IsAlone\", \"TitleEncoded\"],\n \"algorithms\": [\"LogisticRegression\", \"RandomForest\", \"XGBoost\"],\n \"evaluation_metric\": \"f1_score\"\n}",
"promptType": "define"
},
"typeVersion": 1.4
},
{
"id": "1f902e16-0c8d-4544-8ad6-032c4bc470b1",
"name": "P1: Anthropic Sonnet",
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
"position": [
7072,
1808
],
"parameters": {
"model": {
"__rl": true,
"mode": "id",
"value": "claude-sonnet-4-6"
},
"options": {}
},
"typeVersion": 1.3
},
{
"id": "efcf1add-b60f-43c7-837e-6ff69b75b725",
"name": "P1: Parse Strategy",
"type": "n8n-nodes-base.code",
"position": [
7360,
1616
],
"parameters": {
"jsCode": "const raw = $input.first().json.text || '';\nlet strategy;\ntry {\n const match = raw.match(/\\{[\\s\\S]*\\}/);\n strategy = match ? JSON.parse(match[0]) : null;\n} catch(e) { strategy = null; }\n\nif (!strategy) {\n strategy = {\n dataset_url: $('P1: Receive MLOps Job').first().json.body.dataset_url,\n target_variable: $('P1: Receive MLOps Job').first().json.body.target_variable,\n business_goal: $('P1: Receive MLOps Job').first().json.body.business_goal,\n cleaning_steps: ['drop_nulls', 'encode_categoricals'],\n feature_ideas: ['FamilySize', 'IsAlone', 'TitleEncoded'],\n algorithms: ['LogisticRegression', 'RandomForest', 'XGBoost'],\n evaluation_metric: 'f1_score'\n };\n}\n\nreturn [{ json: strategy }];"
},
"typeVersion": 2
},
{
"id": "03951aa4-1e28-4caf-90df-aca7e2808408",
"name": "P1: Log Strategy",
"type": "n8n-nodes-base.httpRequest",
"position": [
7584,
1616
],
"parameters": {
"url": "https://YOUR_PROJECT.supabase.co/rest/v1/mlops_audit_log",
"method": "POST",
"options": {},
"sendBody": true,
"sendHeaders": true,
"bodyParameters": {
"parameters": [
{
"name": "phase",
"value": "P1_STRATEGY"
},
{
"name": "status",
"value": "complete"
},
{
"name": "workflow_step",
"value": "={{ 'target=' + $json.target_variable }}"
},
{
"name": "run_id",
"value": "={{ $now.toISO() }}"
}
]
},
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "YOUR_SUPABASE_SERVICE_ROLE_KEY"
},
{
"name": "Authorization",
"value": "Bearer YOUR_TOKEN_HERE"
},
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Prefer",
"value": "return=minimal"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "62f443d2-1562-407c-bfb6-d6db39728757",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
7808,
1408
],
"parameters": {
"color": 6,
"width": 660,
"height": 80,
"content": "### Phase 2 \u2014 Data Engineering\n`HTTP Request` fetches CSV \u2192 `Code` parses + cleans + encodes \u2192 `Supabase` audit log"
},
"typeVersion": 1
},
{
"id": "c0cbdec9-6fe4-4ec2-a703-569184cfbab4",
"name": "P2: Log Data Eng",
"type": "n8n-nodes-base.httpRequest",
"position": [
8064,
1616
],
"parameters": {
"url": "https://YOUR_PROJECT.supabase.co/rest/v1/mlops_audit_log",
"method": "POST",
"options": {},
"sendBody": true,
"sendHeaders": true,
"bodyParameters": {
"parameters": [
{
"name": "phase",
"value": "P2_DATA_ENG"
},
{
"name": "status",
"value": "complete"
},
{
"name": "workflow_step",
"value": "={{ 'rows=' + $json.clean_count }}"
},
{
"name": "run_id",
"value": "={{ $now.toISO() }}"
}
]
},
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "YOUR_SUPABASE_SERVICE_ROLE_KEY"
},
{
"name": "Authorization",
"value": "Bearer YOUR_TOKEN_HERE"
},
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Prefer",
"value": "return=minimal"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "27d37c92-5bf3-4d18-b7e3-91bbb5fd63c0",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
8512,
1408
],
"parameters": {
"color": 5,
"width": 924,
"height": 80,
"content": "### Phase 3 \u2014 Feature Engineering\n`Claude Haiku` approves feature plan \u2192 `Code` engineers FamilySize, IsAlone, TitleEncoded \u2192 `Supabase` audit log"
},
"typeVersion": 1
},
{
"id": "b92d76b4-9cdd-4f4a-bfb3-5380a28753e3",
"name": "P3: Reason About Features",
"type": "@n8n/n8n-nodes-langchain.chainLlm",
"position": [
8560,
1616
],
"parameters": {
"text": "=You are a Feature Engineering expert. Given this dataset context, confirm the 3 best features to engineer. Return raw JSON only, no markdown.\n\nTarget: {{ $('P1: Parse Strategy').first().json.target_variable }}\nSuggested features: {{ JSON.stringify($('P1: Parse Strategy').first().json.feature_ideas) }}\nClean row count: {{ $('P2: Clean Data').first().json.clean_count }}\n\nReturn exactly:\n{\"approved_features\": [\"FamilySize\", \"IsAlone\", \"TitleEncoded\"], \"rationale\": \"<one sentence>\"}",
"promptType": "define"
},
"typeVersion": 1.4
},
{
"id": "aee2ef0d-c1e1-41d6-b88b-a9c1a9e47b6e",
"name": "P3: Anthropic Haiku",
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
"position": [
8560,
1792
],
"parameters": {
"model": {
"__rl": true,
"mode": "id",
"value": "claude-haiku-4-5-20251001"
},
"options": {}
},
"typeVersion": 1.3
},
{
"id": "2cf42398-c4df-4781-a459-0270b8a09030",
"name": "P3: Parse Feature Plan",
"type": "n8n-nodes-base.code",
"position": [
8880,
1616
],
"parameters": {
"jsCode": "const raw = $input.first().json.text || '';\nlet plan;\ntry {\n const match = raw.match(/\\{[\\s\\S]*\\}/);\n plan = match ? JSON.parse(match[0]) : null;\n} catch(e) { plan = null; }\n\nif (!plan || !plan.approved_features) {\n plan = { approved_features: ['FamilySize', 'IsAlone', 'TitleEncoded'], rationale: 'Default fallback features' };\n}\n\n// Pass through all upstream data\nconst upstream = $('P2: Clean Data').first().json;\nreturn [{ json: { ...upstream, approved_features: plan.approved_features, feature_rationale: plan.rationale } }];"
},
"typeVersion": 2
},
{
"id": "af43c97a-251b-419f-a01d-2a69a90014ed",
"name": "P3: Engineer Features",
"type": "n8n-nodes-base.code",
"position": [
9072,
1616
],
"parameters": {
"jsCode": "const upstream = $input.first().json;\nconst cleanData = JSON.parse(upstream.clean_data);\n\n// Engineer features\nconst titleMap = { 'Mr': 1, 'Mrs': 2, 'Miss': 3, 'Master': 4, 'Dr': 5, 'Rev': 6 };\n\nconst engineered = cleanData.map(row => {\n const sibsp = parseFloat(row['SibSp']) || 0;\n const parch = parseFloat(row['Parch']) || 0;\n const familySize = sibsp + parch + 1;\n const isAlone = familySize === 1 ? 1 : 0;\n \n const name = row['Name'] || '';\n const titleMatch = name.match(/,\\s*([A-Za-z]+)\\./);\n const titleStr = titleMatch ? titleMatch[1] : 'Mr';\n const titleEnc = titleMap[titleStr] || 1;\n \n return {\n ...row,\n FamilySize: familySize,\n IsAlone: isAlone,\n TitleEncoded: titleEnc\n };\n});\n\n// Validate row count\nif (engineered.length !== cleanData.length) {\n throw new Error(`Row count mismatch: ${cleanData.length} \u2192 ${engineered.length}`);\n}\n\nreturn [{ json: {\n strategy: upstream.strategy,\n clean_count: upstream.clean_count,\n feature_count: engineered.length,\n approved_features: upstream.approved_features,\n engineered_data: JSON.stringify(engineered)\n}}];"
},
"typeVersion": 2
},
{
"id": "36569900-b3d4-468c-b8ef-6048045cf799",
"name": "P3: Log Feature Eng",
"type": "n8n-nodes-base.httpRequest",
"position": [
9296,
1616
],
"parameters": {
"url": "https://YOUR_PROJECT.supabase.co/rest/v1/mlops_audit_log",
"method": "POST",
"options": {},
"sendBody": true,
"sendHeaders": true,
"bodyParameters": {
"parameters": [
{
"name": "phase",
"value": "P3_FEATURE_ENG"
},
{
"name": "status",
"value": "complete"
},
{
"name": "workflow_step",
"value": "={{ 'features=' + $json.feature_count }}"
},
{
"name": "run_id",
"value": "={{ $now.toISO() }}"
}
]
},
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "YOUR_SUPABASE_SERVICE_ROLE_KEY"
},
{
"name": "Authorization",
"value": "Bearer YOUR_TOKEN_HERE"
},
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Prefer",
"value": "return=minimal"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "e62131bf-706e-4445-9ee2-f4e9fe61389d",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
9472,
1408
],
"parameters": {
"color": 4,
"width": 1060,
"height": 80,
"content": "### Phase 4 \u2014 Training & Evaluation\n`Code` trains Logistic Regression + Random Forest + XGBoost \u2192 `Claude Sonnet` judges winner by F1 score \u2192 `Supabase` audit log"
},
"typeVersion": 1
},
{
"id": "ce1db2b8-6a2b-4a56-97f2-86f4044b505e",
"name": "P4: Setup Algorithms",
"type": "n8n-nodes-base.code",
"position": [
9504,
1616
],
"parameters": {
"jsCode": "const upstream = $('P3: Engineer Features').first().json;\nconst p3data = $('P3: Engineer Features').first().json;\n\n// Parse engineered data once\nconst allData = JSON.parse(p3data.engineered_data);\nconst targetVar = p3data.strategy.target_variable;\n\n// Feature columns to use\nconst featureCols = ['Pclass', 'Age', 'SibSp', 'Parch', 'Fare', 'Sex_enc', 'Embarked_enc', 'FamilySize', 'IsAlone', 'TitleEncoded'];\n\n// Build X and y\nconst X = allData.map(row => featureCols.map(f => {\n const v = parseFloat(row[f]);\n return isNaN(v) ? 0 : v;\n}));\nconst y = allData.map(row => parseFloat(row[targetVar]) || 0);\n\n// Train/test split (80/20)\nconst splitIdx = Math.floor(X.length * 0.8);\nconst Xtrain = X.slice(0, splitIdx);\nconst ytrain = y.slice(0, splitIdx);\nconst Xtest = X.slice(splitIdx);\nconst ytest = y.slice(splitIdx);\n\nreturn [{ json: {\n strategy: p3data.strategy,\n featureCols,\n Xtrain: JSON.stringify(Xtrain),\n ytrain: JSON.stringify(ytrain),\n Xtest: JSON.stringify(Xtest),\n ytest: JSON.stringify(ytest),\n train_size: Xtrain.length,\n test_size: Xtest.length\n}}];"
},
"typeVersion": 2
},
{
"id": "f0bec516-fdd9-4b95-a27b-eaab6d12651c",
"name": "P4: Train All Models",
"type": "n8n-nodes-base.code",
"position": [
9728,
1616
],
"parameters": {
"jsCode": "const setup = $input.first().json;\nconst Xtrain = JSON.parse(setup.Xtrain);\nconst ytrain = JSON.parse(setup.ytrain);\nconst Xtest = JSON.parse(setup.Xtest);\nconst ytest = JSON.parse(setup.ytest);\n\n// --- Helpers ---\nfunction dot(a, b) { return a.reduce((s, v, i) => s + v * b[i], 0); }\nfunction sigmoid(z) { return 1 / (1 + Math.exp(-Math.max(-500, Math.min(500, z)))); }\n\nfunction calcMetrics(ypred, ytrue) {\n let tp=0, fp=0, fn=0, tn=0;\n for (let i=0; i<ytrue.length; i++) {\n if (ypred[i]===1 && ytrue[i]===1) tp++;\n else if (ypred[i]===1 && ytrue[i]===0) fp++;\n else if (ypred[i]===0 && ytrue[i]===1) fn++;\n else tn++;\n }\n const prec = tp+fp>0 ? tp/(tp+fp) : 0;\n const rec = tp+fn>0 ? tp/(tp+fn) : 0;\n const f1 = prec+rec>0 ? 2*prec*rec/(prec+rec) : 0;\n const acc = (tp+tn)/ytrue.length;\n return { accuracy: Math.round(acc*1000)/1000, precision: Math.round(prec*1000)/1000, recall: Math.round(rec*1000)/1000, f1_score: Math.round(f1*1000)/1000 };\n}\n\n// --- 1. Logistic Regression (gradient descent) ---\nfunction trainLogReg(X, y, lr=0.01, epochs=200) {\n const n = X[0].length;\n let w = new Array(n).fill(0);\n let b = 0;\n for (let e=0; e<epochs; e++) {\n let dw = new Array(n).fill(0), db = 0;\n for (let i=0; i<X.length; i++) {\n const pred = sigmoid(dot(X[i], w) + b);\n const err = pred - y[i];\n for (let j=0; j<n; j++) dw[j] += err * X[i][j];\n db += err;\n }\n for (let j=0; j<n; j++) w[j] -= lr * dw[j] / X.length;\n b -= lr * db / X.length;\n }\n return { w, b };\n}\nconst lrModel = trainLogReg(Xtrain, ytrain);\nconst lrPred = Xtest.map(x => sigmoid(dot(x, lrModel.w) + lrModel.b) >= 0.5 ? 1 : 0);\nconst lrMetrics = calcMetrics(lrPred, ytest);\n\n// --- 2. Random Forest (bagged decision stumps) ---\nfunction trainStump(X, y, featureIdx) {\n let bestThresh = 0, bestGini = 1, bestDir = 1;\n const vals = X.map(row => row[featureIdx]).sort((a,b) => a-b);\n const thresholds = vals.filter((v,i) => i===0 || v !== vals[i-1]);\n for (const t of thresholds) {\n for (const dir of [1, -1]) {\n const pred = X.map(row => (dir*(row[featureIdx]-t) >= 0) ? 1 : 0);\n let tp=0,tn=0,fp=0,fn=0;\n pred.forEach((p,i) => { if(p===1&&y[i]===1)tp++; else if(p===0&&y[i]===0)tn++; else if(p===1)fp++; else fn++; });\n const n1=tp+fp, n0=tn+fn, tot=X.length;\n const g1=n1>0?1-Math.pow(tp/n1,2)-Math.pow(fp/n1,2):0;\n const g0=n0>0?1-Math.pow(tn/n0,2)-Math.pow(fn/n0,2):0;\n const gini=(n1*g1+n0*g0)/tot;\n if(gini<bestGini){bestGini=gini;bestThresh=t;bestDir=dir;}\n }\n }\n return { featureIdx, threshold: bestThresh, direction: bestDir };\n}\n\nconst numFeatures = Xtrain[0].length;\nconst trees = [];\nfor (let t=0; t<10; t++) {\n const bagSize = Math.floor(Xtrain.length * 0.8);\n const bagIdx = Array.from({length: bagSize}, () => Math.floor(Math.random()*Xtrain.length));\n const Xbag = bagIdx.map(i => Xtrain[i]);\n const ybag = bagIdx.map(i => ytrain[i]);\n const fIdx = Math.floor(Math.random() * numFeatures);\n trees.push(trainStump(Xbag, ybag, fIdx));\n}\nconst rfPred = Xtest.map(x => {\n const votes = trees.map(s => (s.direction*(x[s.featureIdx]-s.threshold) >= 0) ? 1 : 0);\n return votes.reduce((a,b)=>a+b,0) >= trees.length/2 ? 1 : 0;\n});\nconst rfMetrics = calcMetrics(rfPred, ytest);\n\n// --- 3. XGBoost Stumps (gradient boosting) ---\nfunction xgbBoost(X, y, nRounds=20, lr=0.3) {\n let F = new Array(X.length).fill(0);\n const stumps = [];\n for (let r=0; r<nRounds; r++) {\n const residuals = F.map((f,i) => y[i] - sigmoid(f));\n let bestStump = null, bestCorr = -Infinity;\n for (let fi=0; fi<X[0].length; fi++) {\n const vals = [...new Set(X.map(row => row[fi]))].sort((a,b)=>a-b);\n for (const t of vals) {\n const pred = X.map(row => row[fi] >= t ? 1 : -1);\n const corr = pred.reduce((s,p,i) => s + p*residuals[i], 0);\n if (Math.abs(corr) > bestCorr) { bestCorr = Math.abs(corr); bestStump = {fi, t, dir: corr>0?1:-1}; }\n }\n }\n if (!bestStump) break;\n stumps.push(bestStump);\n F = F.map((f,i) => f + lr * bestStump.dir * (X[i][bestStump.fi] >= bestStump.t ? 1 : -1));\n }\n return { stumps, F_train: F };\n}\nconst xgbModel = xgbBoost(Xtrain, ytrain);\nconst xgbPredF = Xtest.map(x => {\n let f = 0;\n for (const s of xgbModel.stumps) f += 0.3 * s.dir * (x[s.fi] >= s.t ? 1 : -1);\n return f;\n});\nconst xgbPred = xgbPredF.map(f => sigmoid(f) >= 0.5 ? 1 : 0);\nconst xgbMetrics = calcMetrics(xgbPred, ytest);\n\nconst results = [\n { algorithm: 'LogisticRegression', ...lrMetrics },\n { algorithm: 'RandomForest', ...rfMetrics },\n { algorithm: 'XGBoost', ...xgbMetrics }\n];\n\nreturn [{ json: {\n strategy: setup.strategy,\n train_size: setup.train_size,\n test_size: setup.test_size,\n model_results: results,\n model_results_str: JSON.stringify(results)\n}}];"
},
"typeVersion": 2
},
{
"id": "dd1ad86a-9469-4eb7-890f-706fb1cf9de9",
"name": "P4: LLM Judge Best Model",
"type": "@n8n/n8n-nodes-langchain.chainLlm",
"position": [
9888,
1616
],
"parameters": {
"text": "=You are an ML expert judge. Analyze these model results and select the best model. Return raw JSON only, no markdown.\n\nBusiness goal: {{ $json.strategy.business_goal }}\nEvaluation metric: {{ $json.strategy.evaluation_metric }}\nModel results: {{ $json.model_results_str }}\n\nReturn exactly:\n{\"winner\": \"<algorithm name>\", \"f1_score\": <number>, \"accuracy\": <number>, \"justification\": \"<one sentence why this model wins>\"}",
"promptType": "define"
},
"typeVersion": 1.4
},
{
"id": "952afba0-febf-424d-ae1c-8990f240e404",
"name": "P4: Anthropic Sonnet Judge",
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
"position": [
9888,
1792
],
"parameters": {
"model": {
"__rl": true,
"mode": "id",
"value": "claude-sonnet-4-6"
},
"options": {}
},
"typeVersion": 1.3
},
{
"id": "38cd85dd-8565-4607-99dc-dabcc462fb7d",
"name": "P4: Parse Judge Verdict",
"type": "n8n-nodes-base.code",
"position": [
10176,
1616
],
"parameters": {
"jsCode": "const raw = $input.first().json.text || '';\nlet verdict;\ntry {\n const match = raw.match(/\\{[\\s\\S]*\\}/);\n verdict = match ? JSON.parse(match[0]) : null;\n} catch(e) { verdict = null; }\n\nconst upstream = $('P4: Train All Models').first().json;\nconst results = upstream.model_results;\n\nif (!verdict || !verdict.winner) {\n // Fallback: pick highest f1\n const best = results.reduce((a,b) => b.f1_score > a.f1_score ? b : a);\n verdict = { winner: best.algorithm, f1_score: best.f1_score, accuracy: best.accuracy, justification: 'Selected by highest F1 score' };\n}\n\nreturn [{ json: {\n strategy: upstream.strategy,\n model_results: results,\n winner: verdict.winner,\n winner_f1: verdict.f1_score,\n winner_accuracy: verdict.accuracy,\n justification: verdict.justification,\n train_size: upstream.train_size,\n test_size: upstream.test_size\n}}];"
},
"typeVersion": 2
},
{
"id": "971117dd-c2ea-4031-a8eb-be5b994f819f",
"name": "P4: Log Training",
"type": "n8n-nodes-base.httpRequest",
"position": [
10384,
1616
],
"parameters": {
"url": "https://YOUR_PROJECT.supabase.co/rest/v1/mlops_audit_log",
"method": "POST",
"options": {},
"sendBody": true,
"sendHeaders": true,
"bodyParameters": {
"parameters": [
{
"name": "phase",
"value": "P4_TRAINING"
},
{
"name": "status",
"value": "complete"
},
{
"name": "workflow_step",
"value": "={{ 'winner=' + $json.winner + ' f1=' + $json.winner_f1 }}"
},
{
"name": "run_id",
"value": "={{ $now.toISO() }}"
}
]
},
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "YOUR_SUPABASE_SERVICE_ROLE_KEY"
},
{
"name": "Authorization",
"value": "Bearer YOUR_TOKEN_HERE"
},
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Prefer",
"value": "return=minimal"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "b0f56987-47b9-4e53-a779-09b2b163a734",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
10576,
1408
],
"parameters": {
"color": 2,
"width": 936,
"height": 116,
"content": "### Phase 5 \u2014 HITL Deployment\n`Claude Sonnet` writes MODEL_CARD.md \u2192 `Slack` posts approval request to human \u2192 `Supabase` audit log\n\n> \ud83d\udca1 Add a Webhook + If node after the Slack message to handle the human approval callback and trigger a GitHub commit."
},
"typeVersion": 1
},
{
"id": "c41e615a-34da-440b-aa98-1a1967663cbb",
"name": "P5: Generate Model Card",
"type": "@n8n/n8n-nodes-langchain.chainLlm",
"position": [
10608,
1616
],
"parameters": {
"text": "=You are a ML documentation expert. Generate a concise MODEL_CARD.md for the winning model. Use proper markdown.\n\nWinning Model: {{ $json.winner }}\nF1 Score: {{ $json.winner_f1 }}\nAccuracy: {{ $json.winner_accuracy }}\nJustification: {{ $json.justification }}\nBusiness Goal: {{ $json.strategy.business_goal }}\nTarget Variable: {{ $json.strategy.target_variable }}\nTrain Size: {{ $json.train_size }}\nTest Size: {{ $json.test_size }}\nAll Model Results: {{ JSON.stringify($json.model_results) }}\n\nGenerate a MODEL_CARD.md with sections: Model Overview, Performance Metrics, Training Data, Feature Engineering, Intended Use, Limitations.",
"promptType": "define"
},
"typeVersion": 1.4
},
{
"id": "feceb4a1-5121-4a3c-be70-0335c852ae7c",
"name": "P5: Anthropic Sonnet Card",
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
"position": [
10608,
1792
],
"parameters": {
"model": {
"__rl": true,
"mode": "id",
"value": "claude-sonnet-4-6"
},
"options": {}
},
"typeVersion": 1.3
},
{
"id": "62091e00-e3ed-48cc-9e1c-617b25525571",
"name": "P5: Assemble Deployment Payload",
"type": "n8n-nodes-base.code",
"position": [
10832,
1616
],
"parameters": {
"jsCode": "const modelCard = $input.first().json.text || '# Model Card\\n\\nNo content generated.';\nconst verdict = $('P4: Parse Judge Verdict').first().json;\n\nreturn [{ json: {\n strategy: verdict.strategy,\n winner: verdict.winner,\n winner_f1: verdict.winner_f1,\n winner_accuracy: verdict.winner_accuracy,\n justification: verdict.justification,\n model_results: verdict.model_results,\n model_card: modelCard,\n model_card_b64: Buffer.from(modelCard).toString('base64'),\n run_id: `mlops-${Date.now()}`\n}}];"
},
"typeVersion": 2
},
{
"id": "b3c654ae-9026-4922-9da3-7707c8c1ddbc",
"name": "P5: Send Slack Approval",
"type": "n8n-nodes-base.slack",
"position": [
11056,
1616
],
"parameters": {
"text": "=*MLOps Pipeline Complete \u2014 Approval Required*\n\n*Run ID:* `{{ $json.run_id }}`\n*Winner:* {{ $json.winner }}\n*F1 Score:* {{ $json.winner_f1 }}\n*Accuracy:* {{ $json.winner_accuracy }}\n*Justification:* {{ $json.justification }}\n\n*All Results:*\n{{ $json.model_results.map(m => `\u2022 ${m.algorithm}: F1=${m.f1_score}, Acc=${m.accuracy}`).join('\\n') }}\n\nReply with \u2705 to approve GitHub deployment or \u274c to reject.",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "id",
"value": "YOUR_SLACK_CHANNEL_ID"
},
"otherOptions": {}
},
"typeVersion": 2.2
},
{
"id": "b3e57af3-b2f5-4575-981d-fb321530016f",
"name": "P5: Log Pipeline Complete",
"type": "n8n-nodes-base.httpRequest",
"position": [
11264,
1616
],
"parameters": {
"url": "https://YOUR_PROJECT.supabase.co/rest/v1/mlops_audit_log",
"method": "POST",
"options": {},
"sendBody": true,
"sendHeaders": true,
"bodyParameters": {
"parameters": [
{
"name": "phase",
"value": "P5_COMPLETE"
},
{
"name": "status",
"value": "complete"
},
{
"name": "workflow_step",
"value": "={{ 'winner=' + $json.winner }}"
},
{
"name": "run_id",
"value": "={{ $now.toISO() }}"
}
]
},
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "YOUR_SUPABASE_SERVICE_ROLE_KEY"
},
{
"name": "Authorization",
"value": "Bearer YOUR_TOKEN_HERE"
},
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Prefer",
"value": "return=minimal"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "20b42d3e-7c68-4727-9c8c-d2ae274d7389",
"name": "P2: Fetch CSV",
"type": "n8n-nodes-base.httpRequest",
"position": [
7856,
1616
],
"parameters": {
"url": "={{ $('P1: Parse Strategy').first().json.dataset_url }}",
"options": {}
},
"typeVersion": 4.2
},
{
"id": "6c0989bf-1806-4cdc-a7dc-798eb72eeebd",
"name": "P2: Clean Data",
"type": "n8n-nodes-base.code",
"position": [
8288,
1616
],
"parameters": {
"jsCode": "const csvText = $input.first().json.data || '';\nconst strategy = $('P1: Parse Strategy').first().json;\nconst targetVar = strategy.target_variable;\n\nif (!csvText) throw new Error('No CSV data received');\n\n// Proper CSV parser that handles quoted fields with commas\nfunction parseCSVLine(line) {\n const result = [];\n let cur = '';\n let inQuotes = false;\n for (let i = 0; i < line.length; i++) {\n const ch = line[i];\n if (ch === '\"') {\n if (inQuotes && line[i+1] === '\"') { cur += '\"'; i++; }\n else inQuotes = !inQuotes;\n } else if (ch === ',' && !inQuotes) {\n result.push(cur.trim());\n cur = '';\n } else {\n cur += ch;\n }\n }\n result.push(cur.trim());\n return result;\n}\n\nconst lines = csvText.trim().split('\\n');\nconst headers = parseCSVLine(lines[0]);\nconst rows = [];\nfor (let i = 1; i < lines.length; i++) {\n if (!lines[i].trim()) continue;\n const vals = parseCSVLine(lines[i]);\n if (vals.length >= headers.length - 1) {\n const row = {};\n headers.forEach((h, idx) => { row[h] = vals[idx] || ''; });\n rows.push(row);\n }\n}\n\n// Drop rows with missing target\nconst cleaned = rows.filter(r => r[targetVar] !== '' && r[targetVar] !== undefined);\n\n// Numeric conversion\nconst numericFields = ['Age', 'Fare', 'SibSp', 'Parch', 'Pclass', 'PassengerId', targetVar];\ncleaned.forEach(row => {\n numericFields.forEach(f => {\n if (row[f] !== undefined && row[f] !== '') {\n const n = parseFloat(row[f]);\n if (!isNaN(n)) row[f] = n;\n }\n });\n if (typeof row['Age'] !== 'number' || isNaN(row['Age'])) row['Age'] = 29;\n row['Sex_enc'] = row['Sex'] === 'male' ? 0 : row['Sex'] === 'female' ? 1 : -1;\n const embMap = { 'S': 0, 'C': 1, 'Q': 2 };\n row['Embarked_enc'] = embMap[row['Embarked']] !== undefined ? embMap[row['Embarked']] : 0;\n});\n\nreturn [{ json: {\n strategy,\n original_count: rows.length,\n clean_count: cleaned.length,\n headers,\n clean_data: JSON.stringify(cleaned)\n}}];"
},
"typeVersion": 2
}
],
"settings": {
"executionOrder": "v1"
},
"connections": {
"P2: Fetch CSV": {
"main": [
[
{
"node": "P2: Clean Data",
"type": "main",
"index": 0
}
]
]
},
"P2: Clean Data": {
"main": [
[
{
"node": "P2: Log Data Eng",
"type": "main",
"index": 0
},
{
"node": "P3: Reason About Features",
"type": "main",
"index": 0
}
]
]
},
"P1: Parse Strategy": {
"main": [
[
{
"node": "P1: Log Strategy",
"type": "main",
"index": 0
},
{
"node": "P2: Fetch CSV",
"type": "main",
"index": 0
}
]
]
},
"P3: Anthropic Haiku": {
"ai_languageModel": [
[
{
"node": "P3: Reason About Features",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"P1: Anthropic Sonnet": {
"ai_languageModel": [
[
{
"node": "P1: Plan ML Strategy",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"P1: Plan ML Strategy": {
"main": [
[
{
"node": "P1: Parse Strategy",
"type": "main",
"index": 0
}
]
]
},
"P4: Setup Algorithms": {
"main": [
[
{
"node": "P4: Train All Models",
"type": "main",
"index": 0
}
]
]
},
"P4: Train All Models": {
"main": [
[
{
"node": "P4: LLM Judge Best Model",
"type": "main",
"index": 0
}
]
]
},
"P1: Receive MLOps Job": {
"main": [
[
{
"node": "P1: Plan ML Strategy",
"type": "main",
"index": 0
}
]
]
},
"P3: Engineer Features": {
"main": [
[
{
"node": "P3: Log Feature Eng",
"type": "main",
"index": 0
},
{
"node": "P4: Setup Algorithms",
"type": "main",
"index": 0
}
]
]
},
"P3: Parse Feature Plan": {
"main": [
[
{
"node": "P3: Engineer Features",
"type": "main",
"index": 0
}
]
]
},
"P4: Parse Judge Verdict": {
"main": [
[
{
"node": "P4: Log Training",
"type": "main",
"index": 0
},
{
"node": "P5: Generate Model Card",
"type": "main",
"index": 0
}
]
]
},
"P5: Generate Model Card": {
"main": [
[
{
"node": "P5: Assemble Deployment Payload",
"type": "main",
"index": 0
}
]
]
},
"P4: LLM Judge Best Model": {
"main": [
[
{
"node": "P4: Parse Judge Verdict",
"type": "main",
"index": 0
}
]
]
},
"P3: Reason About Features": {
"main": [
[
{
"node": "P3: Parse Feature Plan",
"type": "main",
"index": 0
}
]
]
},
"P5: Anthropic Sonnet Card": {
"ai_languageModel": [
[
{
"node": "P5: Generate Model Card",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"P4: Anthropic Sonnet Judge": {
"ai_languageModel": [
[
{
"node": "P4: LLM Judge Best Model",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"P5: Assemble Deployment Payload": {
"main": [
[
{
"node": "P5: Send Slack Approval",
"type": "main",
"index": 0
},
{
"node": "P5: Log Pipeline Complete",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow automates the full machine learning lifecycle end-to-end using Claude AI as the intelligent decision-maker at every stage. Send one HTTP request with a dataset URL and a business goal — and the pipeline handles everything from raw CSV to a human-approved,…
Source: https://n8n.io/workflows/13781/ — 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.
Automatically reads every reply to your cold email campaigns in Instantly.ai, uses Claude AI to understand the intent, and takes the right action . No need ofmanual inbox checking needed. A lead repli
Requirements: GitHub API token ( scope), Anthropic API key (Claude Sonnet 4.5), Slack Bot Token (optional)
This workflow automates Invoice & Payment Tracking (with Approvals) across Notion and Slack. Ingest — You drop invoices/receipts (PDF/IMG/JSON) into the flow. Extract — OCR + parsing pulls out key fie
Content - Newsletter Agent. Uses formTrigger, chainLlm, outputParserStructured, httpRequest. Event-driven trigger; 87 nodes.
This n8n workflow orchestrates a powerful suite of AI Agents and automations to manage and optimize various aspects of an e-commerce operation, particularly for platforms like Shopify. It leverages La