AutomationFlowsSlack & Telegram › Cv Slack Bot: Drop a Cv in Dm, Get an Instant Summary – Powered by Easybits

Cv Slack Bot: Drop a Cv in Dm, Get an Instant Summary – Powered by Easybits

CV Slack Bot: Drop a CV in DM, Get an Instant Summary – powered by easybits. Uses @easybits/n8n-nodes-extractor, slackTrigger, httpRequest, slack. Event-driven trigger; 18 nodes.

Event trigger★★★★☆ complexity18 nodes@Easybits/N8N Nodes ExtractorSlack TriggerHTTP RequestSlack
Slack & Telegram Trigger: Event Nodes: 18 Complexity: ★★★★☆ Added:

This workflow follows the HTTP Request → Slack 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 →

Download .json
{
  "name": "CV Slack Bot: Drop a CV in DM, Get an Instant Summary \u2013 powered by easybits",
  "nodes": [
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 1
          },
          "conditions": [
            {
              "id": "supported",
              "leftValue": "={{ $json.files?.[0] && ['application/pdf', 'image/png', 'image/jpeg'].includes($json.files[0].mimetype) }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "7369f74d-73af-4459-8971-f6bf22595b65",
      "name": "Guard: Supported MIME",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        -1040,
        240
      ]
    },
    {
      "parameters": {},
      "id": "ba7a0dba-b65e-4ab1-b92c-8071c3c1cc9f",
      "name": "easybits: Extract CV Fields",
      "type": "@easybits/n8n-nodes-extractor.easybitsExtractor",
      "typeVersion": 1,
      "position": [
        -432,
        112
      ],
      "credentials": {
        "easybitsExtractorApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "trigger": [
          "message"
        ],
        "channelId": {
          "__rl": true,
          "value": "YOUR_SLACK_CHANNEL_ID",
          "mode": "id"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.slackTrigger",
      "typeVersion": 1,
      "position": [
        -1648,
        240
      ],
      "id": "60ca752f-7986-4a3f-8aca-195db30f762d",
      "name": "Watch Channel for Messages",
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "url": "={{ $json.files[0].url_private_download }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        -736,
        112
      ],
      "id": "caa90d7d-473b-4f1e-b134-3ea99c48cd5b",
      "name": "Download File from Slack",
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Build both the plain-text summary AND the Block Kit card payload.\n// Defensive: the Extractor returns arrays as JSON-encoded strings sometimes,\n// or comma-separated strings when its parsing fell back. Handle both.\n\nconst toArray = (v) => {\n  if (Array.isArray(v)) return v;\n  if (v == null) return [];\n  if (typeof v === 'string') {\n    const s = v.trim();\n    if (!s) return [];\n    // Try JSON first\n    if (s.startsWith('[')) {\n      try { return JSON.parse(s); } catch (e) { /* fall through */ }\n    }\n    // Fall back to comma split for simple string lists\n    return s.split(',').map(x => x.trim()).filter(Boolean);\n  }\n  return [v];\n};\n\nconst data = $input.first().json.data || {};\nconst trig = $('Watch Channel for Messages').first().json;\nconst meta = {\n  file_name: trig.files?.[0]?.name,\n  user_id: trig.user,\n  channel: trig.channel,\n  thread_ts: trig.ts\n};\n\nconst name = data.full_name || 'Unknown candidate';\nconst location = data.location || null;\nconst yoe = data.total_years_experience;\nconst skills = toArray(data.top_skills);\nconst roles = toArray(data.last_three_roles);\nconst education = toArray(data.education);\nconst salary = data.salary_expectations || null;\nconst linkedin = data.linkedin_url || null;\n\n// --- Plain text summary (for the first message) ---\nconst lines = [`*${name}*`];\nif (location) lines.push(`\ud83d\udccd ${location}`);\nif (yoe != null) lines.push(`\ud83d\udcbc ${yoe} year${yoe === 1 ? '' : 's'} experience`);\nif (skills.length) lines.push(`\ud83d\udee0 ${skills.join(' \u2022 ')}`);\nif (roles.length) {\n  lines.push('');\n  lines.push('*Recent roles*');\n  for (const r of roles) {\n    const period = r.end ? `${r.start || '?'} \u2013 ${r.end}` : (r.start || '');\n    lines.push(`\u2022 ${r.title || '?'} @ ${r.company || '?'}${period ? `  _(${period})_` : ''}`);\n  }\n}\nif (education.length) {\n  lines.push('');\n  lines.push('*Education*');\n  for (const e of education) {\n    lines.push(`\u2022 ${e.degree || '?'}${e.institution ? ` \u2014 ${e.institution}` : ''}${e.year ? ` (${e.year})` : ''}`);\n  }\n}\nif (salary) lines.push(`\\n\ud83d\udcb0 Salary expectations: ${salary}`);\nif (linkedin) lines.push(`\ud83d\udd17 ${linkedin}`);\n\nconst summaryText = lines.join('\\n');\n\n// --- Block Kit card (second message, with action buttons) ---\n// Carries the extracted data in the button's value field so Workflow B\n// can append to Sheet without re-running the Extractor.\nconst payload = {\n  candidate_name: name,\n  location,\n  total_years_experience: yoe,\n  top_skills: skills,\n  last_three_roles: roles,\n  education,\n  salary_expectations: salary,\n  linkedin_url: linkedin,\n  file_name: meta.file_name,\n  source_user: meta.user_id,\n  source_channel: meta.channel,\n  source_thread: meta.thread_ts\n};\n\n// Slack action value has a 2000 char limit. JSON of typical CV fits easily,\n// but truncate roles/education descriptions just in case.\nconst valueStr = JSON.stringify(payload);\nif (valueStr.length > 1900) {\n  // Rare, but fail loud rather than silently corrupting the button payload.\n  throw new Error(`Block Kit action value too large: ${valueStr.length} chars`);\n}\n\nconst card = {\n  blocks: [\n    {\n      type: 'section',\n      text: { type: 'mrkdwn', text: `What do you want to do with *${name}*?` }\n    },\n    {\n      type: 'actions',\n      block_id: 'cv_actions',\n      elements: [\n        {\n          type: 'button',\n          action_id: 'save_to_sheet',\n          style: 'primary',\n          text: { type: 'plain_text', text: '\ud83d\udcbe Save to Sheet' },\n          value: valueStr\n        },\n        {\n          type: 'button',\n          action_id: 'dismiss',\n          text: { type: 'plain_text', text: 'Dismiss' },\n          value: 'dismiss'\n        }\n      ]\n    }\n  ]\n};\n\nreturn [{\n  json: {\n    summary_text: summaryText,\n    card_blocks: card.blocks,\n    channel: meta.channel,\n    thread_ts: meta.thread_ts\n  }\n}];"
      },
      "id": "8df69140-7177-4b1c-ad93-954c8021337b",
      "name": "Build Summary & Action Card",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -224,
        112
      ]
    },
    {
      "parameters": {
        "select": "channel",
        "channelId": {
          "__rl": true,
          "value": "YOUR_SLACK_CHANNEL_ID",
          "mode": "id"
        },
        "text": "={{ $json.summary_text }}",
        "otherOptions": {
          "thread_ts": {
            "replyValues": {
              "thread_ts": "={{ $json.thread_ts }}"
            }
          },
          "mrkdwn": true
        }
      },
      "id": "bd7309bf-35f5-4f08-9835-ea4fd2e0abf7",
      "name": "Post Summary to Thread",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        80,
        -48
      ],
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://slack.com/api/chat.postMessage",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json; charset=utf-8"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ {\n  channel: $json.channel,\n  thread_ts: $json.thread_ts,\n  text: \"Action menu\",\n  blocks: $json.card_blocks\n} }}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        80,
        400
      ],
      "id": "9278dc56-24d4-4765-9bbb-68e66db54fbb",
      "name": "Post Action Card to Slack",
      "credentials": {
        "httpBearerAuth": {
          "name": "<your credential>"
        },
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "select": "channel",
        "channelId": {
          "__rl": true,
          "value": "YOUR_SLACK_CHANNEL_ID",
          "mode": "id"
        },
        "text": "=\u26a0\ufe0f I can only read PDF, PNG, or JPG files. Got `{{ $('Watch Channel for Messages').first().json.files?.[0]?.mimetype }}`.",
        "otherOptions": {
          "thread_ts": {
            "replyValues": {
              "thread_ts": "={{ $('Watch Channel for Messages').first().json.event_ts || $('Watch Channel for Messages').first().json.ts }}"
            }
          }
        }
      },
      "id": "5fb99fa6-30e4-4683-bcbe-b2a71c4e52cb",
      "name": "Reply: Unsupported File Type",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        -736,
        560
      ],
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 1
          },
          "conditions": [
            {
              "id": "not-bot",
              "leftValue": "={{ $json.user_id || $json.user }}",
              "rightValue": "=UYOURBOTID",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              }
            },
            {
              "id": "a773d2e4-66da-42ed-8bdf-13cf5ce0df74",
              "leftValue": "={{ $json.files?.[0] ? true : false }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "5c5bfbbd-70e7-4986-998e-46b626eb0d5c",
      "name": "Guard: Self & No-File Filter",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        -1344,
        240
      ]
    },
    {
      "parameters": {
        "content": "## \ud83d\udc40 Watch Channel for Messages\nListens for every new message in the configured channel. Plain text messages and unsupported file types are filtered out by the guards downstream, so this trigger stays simple and catches everything.",
        "height": 432,
        "width": 288,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1744,
        -16
      ],
      "typeVersion": 1,
      "id": "2c48185b-f7a3-4413-b24d-6e0d67ae8980",
      "name": "Sticky Note"
    },
    {
      "parameters": {
        "content": "## \ud83e\ude9e Guard: Ignore Self & No-File Filter\nTwo checks: ignore the bot's own posts (otherwise it'd react to its own replies in a loop), and silently skip plain text messages with no file attached. Replace `UYOURBOTID` with your bot's Slack user ID \u2013 find it under your Slack app's **Bot User** settings.",
        "height": 432,
        "width": 288,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1440,
        -16
      ],
      "typeVersion": 1,
      "id": "7a7afef1-f291-466a-9648-7c77063afc2c",
      "name": "Sticky Note1"
    },
    {
      "parameters": {
        "content": "## \ud83d\udeab Reply: Unsupported File Type\nFires only when someone uploads an actual file the Extractor can't read \u2013 Word docs, videos, archives, etc. Plain text messages are already filtered out upstream, so recruiters who just chat in the channel don't get spammed with error replies.",
        "height": 432,
        "width": 288,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -832,
        304
      ],
      "typeVersion": 1,
      "id": "feeccd6a-1982-4e4f-9ab1-e1b05706e59e",
      "name": "Sticky Note3"
    },
    {
      "parameters": {
        "content": "## \ud83d\udec2 Guard: Supported MIME\nOnly `application/pdf`, `image/png`, and `image/jpeg` pass through. The Extractor doesn't accept Word docs, so DOCX is rejected with a friendly Slack reply asking the recruiter to share a PDF instead.",
        "height": 432,
        "width": 288,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1136,
        -16
      ],
      "typeVersion": 1,
      "id": "9dc00ccf-ea9a-41c2-88c8-bac12ddbf77a",
      "name": "Sticky Note2"
    },
    {
      "parameters": {
        "content": "## \u2b07\ufe0f Download File from Slack\nSlack's file URLs are private \u2013 they need a `Bearer xoxb-...` token to download. Uses a **Header Auth** credential (name: `Authorization`, value: `Bearer <your-bot-token>`) to fetch the binary, which is then passed to the Extractor.",
        "height": 432,
        "width": 288,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -832,
        -144
      ],
      "typeVersion": 1,
      "id": "9ea1ff00-0b22-46bf-8e62-865411a6bf6f",
      "name": "Sticky Note4"
    },
    {
      "parameters": {
        "content": "## \ud83e\udde0 Extract & Format\nRuns the CV through the **easybits Extractor** to pull 8 structured fields: `full_name`, `location`, `total_years_experience`, `top_skills`, `last_three_roles`, `education`, `salary_expectations`, `linkedin_url`. The formatter then builds two outputs: a clean Slack summary, and a Block Kit card payload carrying the structured data in the Save button's value field.",
        "height": 432,
        "width": 496,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -528,
        -144
      ],
      "typeVersion": 1,
      "id": "f01681a1-0304-4a76-845e-db5cac1982f9",
      "name": "Sticky Note5"
    },
    {
      "parameters": {
        "content": "## \ud83d\udcac Post Summary to Thread\nPosts a markdown-formatted summary (name, location, years, skills, roles, education) as a reply in the thread of the uploaded CV. Recruiters can read it without leaving the channel.",
        "height": 432,
        "width": 288,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -16,
        -320
      ],
      "typeVersion": 1,
      "id": "7095d2d5-7185-4323-9696-5b10a2f5b102",
      "name": "Sticky Note6"
    },
    {
      "parameters": {
        "content": "## \ud83c\udf9b\ufe0f Post Action Card to Slack\nSends a second message with two interactive buttons: **\ud83d\udcbe Save to Sheet** and **Dismiss**. Uses a direct `chat.postMessage` HTTP call because the n8n Slack node currently has a bug that ignores blocks payloads (`invalid_arguments` / fallback text only). Pairs with **Workflow B** which handles the button clicks.",
        "height": 432,
        "width": 288,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -16,
        128
      ],
      "typeVersion": 1,
      "id": "7a74e82f-b09d-4c0e-95e6-7cc02b9742f1",
      "name": "Sticky Note7"
    },
    {
      "parameters": {
        "content": "# \ud83d\udc4b CV Slack Bot \u2013 Drop a CV, Get an Instant Summary\n(powered by easybits)\n\nA recruiter drops a CV file into a Slack channel. Within seconds, the bot replies in-thread with a structured summary \u2013 name, location, years of experience, top 3 skills, last 3 roles, education, salary expectations, and LinkedIn \u2013 plus an action card to save the candidate to a Google Sheet for follow-up.\n\nBuilt for fast, mid-call lookups (not batch screening). Pairs with **Workflow B** which handles the Save to Sheet button.\n\n## \u2699\ufe0f How It Works\n1. **Watch Channel for Messages** listens for new messages in your CV channel\n2. **Guard: Ignore Self & No-File Filter** skips the bot's own posts and plain text messages\n3. **Guard: Supported MIME** filters to PDF, PNG, or JPG only\n4. **Download File from Slack** fetches the binary using a Bearer token\n5. **easybits: Extract CV Fields** pulls 8 structured fields from the CV\n6. **Build Summary & Action Card** formats the Slack summary + Block Kit payload\n7. **Post Summary to Thread** posts the markdown summary\n8. **Post Action Card to Slack** posts the buttons via direct API call\n\n## \ud83d\udee0\ufe0f Setup Guide\n### 1. Install the easybits Extractor node\n**n8n Cloud:**\n- Open n8n \u2192 **Settings \u2192 Community nodes \u2192 Install**\n- Enter `@easybits/n8n-nodes-extractor` and click install\n- Refresh \u2013 the **easybits Extractor** node will appear in your node panel\n\n**Self-hosted n8n:**\n- SSH into your n8n instance\n- Run: `npm install @easybits/n8n-nodes-extractor` (in your n8n root directory)\n- Restart your n8n instance\n- The Extractor node will appear after restart\n\n**Get your API key:**\n- Sign up at `https://go.easybits.tech/cn` (free plan supports up to 10 fields per Extractor \u2014 this workflow uses 8)\n- Create an Extractor for CVs with the 8 fields documented in the **Extract & Format** sticky\n- Copy your API key, then in n8n: **Credentials \u2192 Add credential \u2192 easybits Extractor API** and paste\n\n### 2. Create the Slack app\nGo to api.slack.com/apps \u2192 **Create New App**:\n- **Bot Token Scopes:** `chat:write`, `files:read`, `channels:history`, `channels:read`\n- **Event Subscriptions:** subscribe to `message.channels`\n- Install to workspace and copy the **Bot User OAuth Token** (`xoxb-...`)\n- Invite the bot to your CV channel: `/invite @YourBotName`\n\n### 3. Set up n8n credentials\n- **Slack API** credential \u2013 for the trigger node\n- **Header Auth** credential \u2013 name: `Authorization`, value: `Bearer xoxb-...` (uses your bot token). Needed for the file download and the action card HTTP call.\n\n### 4. Configure the workflow\n- Set the channel in **Watch Channel for Messages**\n- Replace `UYOURBOTID` in **Guard: Ignore Self & No-File Filter** with your actual bot user ID\n- Attach the Slack and Header Auth credentials to all nodes that need them\n\n### 5. Activate\n- Toggle workflow Active (top right)\n- Copy the trigger's **Production URL**\n- Paste into your Slack app's Event Subscriptions \u2192 Request URL \u2192 Save\n- Drop a CV in the channel to test",
        "height": 1536,
        "width": 704
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2464,
        -560
      ],
      "typeVersion": 1,
      "id": "d48341b3-e939-4b7a-8891-638ee8c42163",
      "name": "Sticky Note8"
    }
  ],
  "connections": {
    "Guard: Supported MIME": {
      "main": [
        [
          {
            "node": "Download File from Slack",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Reply: Unsupported File Type",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "easybits: Extract CV Fields": {
      "main": [
        [
          {
            "node": "Build Summary & Action Card",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Watch Channel for Messages": {
      "main": [
        [
          {
            "node": "Guard: Self & No-File Filter",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download File from Slack": {
      "main": [
        [
          {
            "node": "easybits: Extract CV Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Summary & Action Card": {
      "main": [
        [
          {
            "node": "Post Action Card to Slack",
            "type": "main",
            "index": 0
          },
          {
            "node": "Post Summary to Thread",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Post Action Card to Slack": {
      "main": [
        []
      ]
    },
    "Guard: Self & No-File Filter": {
      "main": [
        [
          {
            "node": "Guard: Supported MIME",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "availableInMCP": false
  },
  "versionId": "",
  "id": "",
  "tags": []
}

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.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

CV Slack Bot: Drop a CV in DM, Get an Instant Summary – powered by easybits. Uses @easybits/n8n-nodes-extractor, slackTrigger, httpRequest, slack. Event-driven trigger; 18 nodes.

Source: https://github.com/felix-sattler-easybits/n8n-workflows/blob/a8138f54ec6b225b7e90e2a66b4491c746767214/easybits-cv-slack-assistant/easybits_cv_slack_assistant_instant_summary.json — original creator credit. Request a take-down →

More Slack & Telegram workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Slack & Telegram

Test Webhooks in n8n Without Changing WEBHOOK_URL (PostBin & BambooHR Example). Uses manualTrigger, stickyNote, httpRequest, postBin. Event-driven trigger; 58 nodes.

HTTP Request, Post Bin, Debug Helper +6
Slack & Telegram

This comprehensive N8N automation template revolutionizes content creation by delivering a complete end-to-end solution for AI-powered blog generation. Transform simple ideas into fully SEO-optimized,

Slack Trigger, Telegram Trigger, Gmail Trigger +16
Slack & Telegram

🧩 What this template does

HTTP Request, Slack, DeepL +3
Slack & Telegram

The Recap AI - VEO 3 Bigfoot Video. Uses formTrigger, lmChatAnthropic, chainLlm, slack. Event-driven trigger; 26 nodes.

Form Trigger, Anthropic Chat, Chain Llm +4
Slack & Telegram

This workflow is designed for n8n users who manage multiple production workflows and want to: Receive intelligent, actionable error alerts instead of raw stack traces Understand root causes without ma

Error Trigger, Chain Llm, Gmail +5