{
  "name": "Vacancy Launch: Calendar, ClickUp & AI LinkedIn Post",
  "tags": [],
  "nodes": [
    {
      "id": "49dd86c0-b87b-4bf8-943e-6f0b90a46876",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        320,
        576
      ],
      "parameters": {
        "path": "calendar-trello-webhook",
        "options": {},
        "httpMethod": "POST"
      },
      "typeVersion": 2.1
    },
    {
      "id": "70dd710a-6943-44de-b066-6ed2f28b98dd",
      "name": "Arranging data",
      "type": "n8n-nodes-base.set",
      "position": [
        560,
        576
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "ac77a4e5-f9f2-4a08-a9a2-00c686273ba3",
              "name": "id",
              "type": "number",
              "value": "={{ $json.body.id }}"
            },
            {
              "id": "ba328827-83f0-4a91-9f3b-f79fcebdf2d3",
              "name": "title",
              "type": "string",
              "value": "={{ $json.body.title }}"
            },
            {
              "id": "2b58450d-956e-4741-887a-48e72327f9ce",
              "name": "sla",
              "type": "string",
              "value": "={{ $json.body.sla }}"
            },
            {
              "id": "c7e1d74e-b9d7-4403-b867-18fe041c451d",
              "name": "expired",
              "type": "string",
              "value": "={{ $json.body.expired }}"
            },
            {
              "id": "7ebe61cb-e417-4bc1-af67-6a961a3adc6a",
              "name": "public_link",
              "type": "string",
              "value": "={{ $json.body.public_link }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "c0ff5b4e-dd47-4ebb-83d8-24d6d0d22bba",
      "name": "Generating vacancy ATS's URL",
      "type": "n8n-nodes-base.code",
      "notes": "This code also returns the end time for the event on the calendar, assuming it is going to be all day long.",
      "position": [
        768,
        576
      ],
      "parameters": {
        "jsCode": "// Percorre todos os itens vindos do input\nreturn items.map(item => {\n  const id = item.json.id;\n\n  // Fun\u00e7\u00e3o interna para tratar a string DD/MM/YYYY e retornar ISO\n  const processDate = (dateStr, addDays = 0) => {\n    if (!dateStr || typeof dateStr !== 'string') return null;\n\n    // Quebra a string \"29/12/2025\"\n    const parts = dateStr.split('/');\n    if (parts.length !== 3) return null;\n\n    // Cria o objeto de data (Ano, M\u00eas-1, Dia)\n    const d = new Date(parts[2], parts[1] - 1, parts[0]);\n    \n    // Adiciona os dias (0 para o in\u00edcio, 1 para o fim)\n    if (addDays > 0) {\n      d.setDate(d.getDate() + addDays);\n    }\n\n    // Define para o in\u00edcio do dia (00:00:00)\n    d.setHours(0, 0, 0, 0);\n\n    // Retorna no formato ISO que o Google Calendar ama\n    return d.toISOString();\n  };\n\n  return {\n    json: {\n      ...item.json,\n      // Datas de In\u00edcio (00:00 do dia)\n      sla: processDate(item.json.sla, 0),\n      expired: processDate(item.json.expired, 0),\n      \n      // Datas de T\u00e9rmino (00:00 do dia seguinte = 24h de dura\u00e7\u00e3o)\n      sla_end: processDate(item.json.sla, 1),\n      expired_end: processDate(item.json.expired, 1),\n\n      // Seu link din\u00e2mico\n      link: `https://app.recrutei.com.br/vacancy/${id}/pipe`\n    }\n  };\n});"
      },
      "notesInFlow": true,
      "typeVersion": 2
    },
    {
      "id": "584de879-a671-4026-a563-2dd21743e807",
      "name": "Create event for the expired date",
      "type": "n8n-nodes-base.googleCalendar",
      "position": [
        1072,
        736
      ],
      "parameters": {
        "end": "={{ $json.expired_end }}",
        "start": "={{ $json.expired }}",
        "calendar": {
          "__rl": true,
          "mode": "list",
          "value": "",
          "cachedResultName": ""
        },
        "additionalFields": {
          "summary": "=Expire date of the vacancy \"{{ $json.title }}\"",
          "description": "=This is the event about the aplication deadline for the following vacancy: {{ $json.title }}\n\nAccess the vacancy here: {{ $json.link }}\n\nThis is the public link: {{ $json.public_link }}"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "bfd448db-ee06-4ea0-b9ea-e495d05794de",
      "name": "Create event for the SLA",
      "type": "n8n-nodes-base.googleCalendar",
      "position": [
        1072,
        576
      ],
      "parameters": {
        "end": "={{ $json.sla_end }}",
        "start": "={{ $json.sla }}",
        "calendar": {
          "__rl": true,
          "mode": "list",
          "value": "",
          "cachedResultName": ""
        },
        "additionalFields": {
          "summary": "=SLA date for {{ $json.title }}",
          "description": "=This is the event about the SLA for the following vacancy: {{ $json.title }}  \n\nAccess the vacancy here: {{ $json.link }}  \n\nThis is the public link: {{ $json.public_link }}"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "f5959c69-6522-40f8-9ec8-e1f4d0597380",
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "position": [
        1408,
        720
      ],
      "parameters": {},
      "typeVersion": 3.2
    },
    {
      "id": "68361506-8fbd-4525-ae48-595a9ec5a689",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        0,
        0
      ],
      "parameters": {
        "width": 944,
        "height": 400,
        "content": "## Overview: Automated Vacancy Launch & AI Marketing\n\nThis workflow streamlines the entire job opening process by connecting your ATS to your operational and marketing tools. It not only manages deadlines but also automates the promotion of the vacancy.\n\n1. **Schedule:** Creates SLA and Expiration events in Google Calendar based on ATS dates.\n2. **Track:** Creates a central task in ClickUp to manage the selection process.\n3. **Content Generation:** Uses GPT-4o to analyze the job description and write a compelling marketing post.\n4. **Publish:** Automatically posts the job to LinkedIn and logs the action back in the ClickUp task.\n\n## Setup steps\n1. **Webhook:** Configure your Recrutei ATS (or similar) to trigger this workflow.\n2. **Google Calendar:** Select the calendar for deadline tracking.\n3. **ClickUp:** Map the Team, Space, and List where vacancy tasks should be created.\n4. **OpenAI:** Ensure you have a valid API Key.\n5. **LinkedIn:** Connect your profile or company page."
      },
      "typeVersion": 1
    },
    {
      "id": "f2be5bb6-0980-459e-b0e1-ab0f5099db26",
      "name": "Create a task for the vacancy",
      "type": "n8n-nodes-base.clickUp",
      "notes": "Refer to the documentation for more information about the credential",
      "position": [
        1648,
        720
      ],
      "parameters": {
        "name": "={{ $('Generating vacancy ATS\\'s URL').item.json.title }}",
        "authentication": "oAuth2",
        "additionalFields": {
          "startDateTime": true
        }
      },
      "executeOnce": true,
      "notesInFlow": true,
      "typeVersion": 1
    },
    {
      "id": "f51b3eea-b525-4ae6-af0c-3f5d57606e0d",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        272,
        416
      ],
      "parameters": {
        "color": 7,
        "width": 656,
        "height": 512,
        "content": "## Data Ingestion\nReceives vacancy details via Webhook. The Code node converts Brazilian date strings (DD/MM/YYYY) into ISO format and calculates the 24h duration required for Google Calendar events."
      },
      "typeVersion": 1
    },
    {
      "id": "8ab39a1f-057c-449b-a673-83f7b077573d",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        928,
        416
      ],
      "parameters": {
        "color": 6,
        "width": 368,
        "height": 512,
        "content": "## Event Creation\nGenerates two distinct entries on the Recruiter's calendar. It maps the vacancy URL and public link into the event description for quick access."
      },
      "typeVersion": 1
    },
    {
      "id": "e558cb31-6e31-4346-a672-ab70047644ea",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1296,
        416
      ],
      "parameters": {
        "color": 7,
        "width": 528,
        "height": 512,
        "content": "## Task Registration\nConsolidates the flow to ensure that, regardless of calendar successes, a task is created in ClickUp to track the vacancy's operational progress."
      },
      "typeVersion": 1
    },
    {
      "id": "b5b27481-f838-4029-b6a0-1e5f8f9fb31a",
      "name": "Create a post",
      "type": "n8n-nodes-base.linkedIn",
      "position": [
        2640,
        720
      ],
      "parameters": {
        "text": "={{ $json.message.content }}",
        "additionalFields": {}
      },
      "typeVersion": 1
    },
    {
      "id": "01c174e5-e095-499f-9242-b8ffc4451594",
      "name": "Message a model",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        2320,
        720
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4.1-mini",
          "cachedResultName": "GPT-4.1-MINI"
        },
        "options": {},
        "messages": {
          "values": [
            {
              "content": "={{ $json.detailedJob }}"
            },
            {
              "role": "system",
              "content": "You will receive information about a job opening; your role is to transform this information into a marketing post to promote and attract candidates via LinkedIn."
            }
          ]
        }
      },
      "typeVersion": 1.8
    },
    {
      "id": "86e3362f-5a24-4471-974e-7b808cdeeb58",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1824,
        416
      ],
      "parameters": {
        "color": 6,
        "width": 416,
        "height": 512,
        "content": "## Preparing vacancy to be publish\nIt recovers the data from the webhook and then inputs into a prompt, ready to "
      },
      "typeVersion": 1
    },
    {
      "id": "92969ead-0f5f-4b10-b92e-30f0cc196183",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2240,
        416
      ],
      "parameters": {
        "color": 7,
        "width": 816,
        "height": 512,
        "content": "## Generating post and registering in vacancy task\nThe agent receives all the data in a single prompt and generates a marketing post to be send to LinkedIn, then, it register this action in the vacancy task in clickup"
      },
      "typeVersion": 1
    },
    {
      "id": "b1923a7f-5dbe-4580-8e97-ca1a43144e1e",
      "name": "Generates a prompt with the vacancy data",
      "type": "n8n-nodes-base.code",
      "position": [
        2064,
        720
      ],
      "parameters": {
        "jsCode": "// Gets the first item from the input array, which contains the job vacancy data.\nconst vagaData = $input.item.json;\n\n// Mapping of original field names to their translated labels and output text.\n// 'skip': true for fields we do not want to include in the final text.\nconst fieldMap = {\n    \"title\": { \"label\": \"Job Title\" },\n    \"manager\": { \"label\": \"Hiring Manager\" },\n    \"quantity\": { \"label\": \"Number of Openings\" },\n    \"client\": { \"label\": \"Client\" },\n    \"department\": { \"label\": \"Department\" },\n    \"pipe\": { \"label\": \"Pipeline / Stage\" },\n    \"internal_code\": { \"label\": \"Internal Code\" },\n    \"status\": { \"label\": \"Status\" },\n    \"type\": { \"label\": \"Job Type\" },\n    \"sla\": { \"label\": \"SLA (Maximum Deadline)\" },\n    \"expired\": { \"label\": \"Expiration Date\" },\n    \"regime\": { \"label\": \"Employment Regime\" },\n    \"public_link\": { \"label\": \"Public Link\" },\n    \"remuneration_type\": { \"label\": \"Compensation Type\" },\n    \"remuneration\": { \"label\": \"Compensation Amount\" },\n    \"fixed_remuneration\": { \"label\": \"Fixed Compensation\" },\n    \"description\": { \"label\": \"Detailed Description\", \"is_long_text\": true },\n    \"skills\": { \"label\": \"Key Skills\" },\n    \"benefits\": { \"label\": \"Benefits\" },\n    \"remote\": { \"label\": \"Remote Work\" },\n    \"location\": { \"label\": \"Location\" },\n    \"country\": { \"label\": \"Country\" },\n    \"state\": { \"label\": \"State\" },\n    \"city\": { \"label\": \"City\" },\n    \"pcd\": { \"label\": \"Position for People with Disabilities (PCD)\" },\n    \"is_inclusive\": { \"label\": \"Inclusive Position\" },\n    \n    // Fields to be ignored\n    \"id\": { \"skip\": true },\n    \"client_id\": { \"skip\": true },\n    \"company_department_id\": { \"skip\": true },\n    \"pipe_id\": { \"skip\": true },\n    \"remuneration_from\": { \"skip\": true },\n    \"remuneration_to\": { \"skip\": true }\n};\n\nlet outputText = \"\";\noutputText += `## Job Details: ${vagaData.title}\\n\\n`;\noutputText += `---\\n\\n`;\n\n// Iterates over the mapping to build the output text\nfor (const key in fieldMap) {\n    if (fieldMap.hasOwnProperty(key) && !fieldMap[key].skip) {\n        const fieldInfo = fieldMap[key];\n        const label = fieldInfo.label;\n        let value = vagaData[key];\n\n        // Handles null or empty values\n        if (value === null || value === \"\" || value === undefined) {\n            value = \"Not Provided\";\n        }\n        \n        // Special formatting for the description (long text, likely HTML)\n        if (fieldInfo.is_long_text) {\n            // Attempts to remove basic HTML tags while keeping list formatting\n            const cleanDescription = String(value)\n                .replace(/<p>|<\\/p>|<br\\s*\\/?>/gi, ' ') // Replace paragraphs and breaks with space\n                .replace(/<h[1-6]>(.*?)<\\/h[1-6]>/gi, '\\n**$1**\\n') // Format headings as bold\n                .replace(/<\\/?ul>|<\\/?ol>/gi, '') // Remove ul/ol tags\n                .replace(/<li>/gi, '  - ') // Format list items\n                .replace(/<\\/?strong>|<\\/?b>/gi, '**') // Bold\n                .replace(/<\\/?em>|<\\/?i>/gi, '*') // Italic\n                .replace(/\\s\\s+/g, ' ') // Remove multiple spaces\n                .trim();\n                \n            outputText += `### ${label}:\\n`;\n            outputText += `${cleanDescription}\\n\\n`;\n        } \n        // Special formatting for compensation\n        else if (key === \"remuneration\" && typeof value === 'number') {\n            const formattedValue = new Intl.NumberFormat('en-US', {\n                style: 'currency',\n                currency: 'USD',\n                minimumFractionDigits: 2\n            }).format(value);\n            outputText += `**${label}:** ${formattedValue}\\n`;\n        } \n        // Default formatting for other fields\n        else {\n            outputText += `**${label}:** ${value}\\n`;\n        }\n    }\n}\n\n// The n8n Code node expects an array of objects as output.\nreturn [{\n    json: {\n        detailedJob: outputText\n    }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "821ec9f0-2d26-4575-8661-5ba3a0d1681d",
      "name": "Recover vacancy data",
      "type": "n8n-nodes-base.code",
      "position": [
        1872,
        720
      ],
      "parameters": {
        "jsCode": "// Fetch items directly from the Webhook node\nconst webhookItems = $items(\"Webhook\");\n\n// Helper function to convert 0/1 into \"no\"/\"yes\"\nconst convertBoolean = (value) => value === 1 ? 'yes' : 'no';\n\n// Map over each Webhook item\nreturn webhookItems.map(item => {\n  const body = item.json.body;\n\n  body.fixed_remuneration = convertBoolean(body.fixed_remuneration);\n  body.remote = convertBoolean(body.remote);\n  body.pcd = convertBoolean(body.pcd);\n  body.is_inclusive = convertBoolean(body.is_inclusive);\n\n  return {\n    json: body\n  };\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "a396d503-e2a8-4104-946b-441ebe14700c",
      "name": "Create a comment in vacancy task",
      "type": "n8n-nodes-base.clickUp",
      "position": [
        2848,
        720
      ],
      "parameters": {
        "id": "={{ $('Create a task for the vacancy').item.json.id }}",
        "resource": "comment",
        "commentOn": "task",
        "commentText": "Vacancy posted in LinkedIn",
        "authentication": "oAuth2",
        "additionalFields": {}
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "",
  "connections": {
    "Merge": {
      "main": [
        [
          {
            "node": "Create a task for the vacancy",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook": {
      "main": [
        [
          {
            "node": "Arranging data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create a post": {
      "main": [
        [
          {
            "node": "Create a comment in vacancy task",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Arranging data": {
      "main": [
        [
          {
            "node": "Generating vacancy ATS's URL",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Message a model": {
      "main": [
        [
          {
            "node": "Create a post",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Recover vacancy data": {
      "main": [
        [
          {
            "node": "Generates a prompt with the vacancy data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create event for the SLA": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generating vacancy ATS's URL": {
      "main": [
        [
          {
            "node": "Create event for the SLA",
            "type": "main",
            "index": 0
          },
          {
            "node": "Create event for the expired date",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create a task for the vacancy": {
      "main": [
        [
          {
            "node": "Recover vacancy data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create event for the expired date": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Generates a prompt with the vacancy data": {
      "main": [
        [
          {
            "node": "Message a model",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}