AutomationFlowsSlack & Telegram › Aulas Sync

Aulas Sync

Aulas Sync. Uses executeWorkflowTrigger, httpRequest, telegram. Event-driven trigger; 9 nodes.

Event trigger★★★★☆ complexity9 nodesExecute Workflow TriggerHTTP RequestTelegram
Slack & Telegram Trigger: Event Nodes: 9 Complexity: ★★★★☆ Added:

This workflow follows the Execute Workflow Trigger → 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 →

Download .json
{
  "name": "Aulas Sync",
  "id": "aulasSync24680",
  "active": true,
  "nodes": [
    {
      "parameters": {},
      "id": "trigger_aulas",
      "name": "Execute Workflow Trigger",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "url": "={{ $env.UTFPR_PORTAL_BASE_URL + '/auth' }}",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ { username: $env.UTFPR_PORTAL_USERNAME, password: $env.UTFPR_PORTAL_PASSWORD } }}",
        "options": {},
        "method": "POST"
      },
      "id": "utfpr_auth",
      "name": "UTFPR Auth",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "onError": "continueRegularOutput",
      "position": [
        200,
        0
      ]
    },
    {
      "parameters": {
        "url": "={{ $env.UTFPR_PORTAL_BASE_URL + '/dados' }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ 'Bearer ' + $json.token }}"
            }
          ]
        },
        "options": {},
        "method": "GET"
      },
      "id": "utfpr_dados",
      "name": "UTFPR Dados",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "onError": "continueRegularOutput",
      "position": [
        400,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "// UTFPR Aulas \u2014 Select student course and preserve auth token\n\nconst authPayload = $items('UTFPR Auth')[0]?.json || {};\nconst dadosPayload = $input.first().json || {};\n\nconst token = String(authPayload.token || '').trim();\nif (!token) {\n  return [{\n    json: {\n      ok: false,\n      error: 'auth_failed',\n      detail: String(authPayload.message || authPayload.error || 'Token ausente no auth').trim()\n    }\n  }];\n}\n\nconst cursosRoot = Array.isArray(dadosPayload?.cursos) ? dadosPayload.cursos : [];\nconst cursosAluno = Array.isArray(dadosPayload?.aluno?.cursos) ? dadosPayload.aluno.cursos : [];\nconst cursos = cursosRoot.length > 0 ? cursosRoot : cursosAluno;\nconst preferredCod = String($env.UTFPR_COD_ALUNO || '').trim();\n\nlet selected = null;\nif (preferredCod) {\n  selected = cursos.find(course => String(course?.alCuIdVc || '') === preferredCod) || null;\n}\nif (!selected) {\n  selected = cursos[0] || null;\n}\n\nif (!selected || !selected.alCuIdVc) {\n  return [{\n    json: {\n      ok: false,\n      error: 'course_not_found',\n      detail: 'Nao foi possivel determinar codAluno em /dados para consultar horario',\n      token\n    }\n  }];\n}\n\nreturn [{\n  json: {\n    ok: true,\n    token,\n    codAluno: String(selected.alCuIdVc),\n    cursoNome: String(selected.cursNomeVc || selected.curso || ''),\n    nomeAluno: String(dadosPayload.pessNomeVc || dadosPayload.nome || ''),\n    ra: String(dadosPayload.login || dadosPayload.ra || ''),\n    preferredCod\n  }\n}];"
      },
      "id": "select_utfpr_course",
      "name": "Select UTFPR Course",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        600,
        0
      ]
    },
    {
      "parameters": {
        "url": "={{ $env.UTFPR_PORTAL_BASE_URL + '/' + encodeURIComponent($json.codAluno || '') + '/horario' }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ 'Bearer ' + $json.token }}"
            }
          ]
        },
        "options": {},
        "method": "GET"
      },
      "id": "utfpr_horario",
      "name": "UTFPR Horario",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "onError": "continueRegularOutput",
      "position": [
        800,
        0
      ]
    },
    {
      "parameters": {
        "url": "={{ $env.UTFPR_PORTAL_BASE_URL + '/' + encodeURIComponent(($items('Select UTFPR Course')[0] && $items('Select UTFPR Course')[0].json && $items('Select UTFPR Course')[0].json.codAluno) || '') + '/boletim' }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ 'Bearer ' + (($items('Select UTFPR Course')[0] && $items('Select UTFPR Course')[0].json && $items('Select UTFPR Course')[0].json.token) || '') }}"
            }
          ]
        },
        "options": {},
        "method": "GET"
      },
      "id": "utfpr_boletim",
      "name": "UTFPR Boletim",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "onError": "continueRegularOutput",
      "position": [
        1000,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "// UTFPR Aulas \u2014 Parse auth/dados/horario/boletim payloads into reusable data\n\nconst TZ = 'America/Sao_Paulo';\nconst DAY_LABELS = {\n  1: 'DOM',\n  2: 'SEG',\n  3: 'TER',\n  4: 'QUA',\n  5: 'QUI',\n  6: 'SEX',\n  7: 'SAB'\n};\n\nconst TURN_ORDER = { M: 0, T: 1, N: 2 };\nconst SLOT_TIMES = {\n  M: {\n    1: ['07:30', '08:20'],\n    2: ['08:20', '09:10'],\n    3: ['09:10', '10:00'],\n    4: ['10:20', '11:10'],\n    5: ['11:10', '12:00'],\n    6: ['12:00', '12:50']\n  },\n  T: {\n    1: ['13:00', '13:50'],\n    2: ['13:50', '14:40'],\n    3: ['14:40', '15:30'],\n    4: ['15:50', '16:40'],\n    5: ['16:40', '17:30'],\n    6: ['17:30', '18:20']\n  },\n  N: {\n    1: ['18:40', '19:30'],\n    2: ['19:30', '20:20'],\n    3: ['20:20', '21:10'],\n    4: ['21:20', '22:10'],\n    5: ['22:10', '23:00']\n  }\n};\n\nfunction safeNumber(v, fallback = 0) {\n  const n = Number(v);\n  return Number.isFinite(n) ? n : fallback;\n}\n\nfunction toMinutes(hhmm) {\n  const [hh, mm] = String(hhmm || '00:00').split(':').map(Number);\n  if (!Number.isFinite(hh) || !Number.isFinite(mm)) return 0;\n  return hh * 60 + mm;\n}\n\nfunction countSlotsInBlock(block) {\n  const start = Number(block?.slotStart);\n  const end = Number(block?.slotEnd);\n  if (Number.isFinite(start) && Number.isFinite(end) && end >= start) {\n    return end - start + 1;\n  }\n  return 1;\n}\n\nfunction shiftHHmm(hhmm, deltaMinutes) {\n  const total = ((toMinutes(hhmm) + deltaMinutes) % 1440 + 1440) % 1440;\n  const hh = String(Math.floor(total / 60)).padStart(2, '0');\n  const mm = String(total % 60).padStart(2, '0');\n  return `${hh}:${mm}`;\n}\n\nfunction nowInfoInTz() {\n  const now = new Date();\n  const weekdayShort = now.toLocaleDateString('en-US', {\n    timeZone: TZ,\n    weekday: 'short'\n  });\n\n  const weekdayMap = {\n    Sun: 1,\n    Mon: 2,\n    Tue: 3,\n    Wed: 4,\n    Thu: 5,\n    Fri: 6,\n    Sat: 7\n  };\n\n  const todayDayNumber = weekdayMap[weekdayShort] || 1;\n\n  const timeParts = new Intl.DateTimeFormat('pt-BR', {\n    timeZone: TZ,\n    hour: '2-digit',\n    minute: '2-digit'\n  }).formatToParts(now);\n\n  const hh = Number(timeParts.find(part => part.type === 'hour')?.value || 0);\n  const mm = Number(timeParts.find(part => part.type === 'minute')?.value || 0);\n  return {\n    todayDayNumber,\n    nowMinutes: hh * 60 + mm\n  };\n}\n\nfunction parseHoraDescr(code) {\n  const raw = String(code || '').trim().toUpperCase();\n  const match = raw.match(/^(\\d)([MTN])(\\d)$/);\n  if (!match) return null;\n\n  const dia = Number(match[1]);\n  const turno = match[2];\n  const slot = Number(match[3]);\n\n  if (!DAY_LABELS[dia]) return null;\n  if (!SLOT_TIMES[turno] || !SLOT_TIMES[turno][slot]) return null;\n\n  return { dia, turno, slot };\n}\n\nfunction parseHorarioPayload(payload) {\n  const rows = Array.isArray(payload) ? payload : [];\n  const slots = [];\n\n  rows.forEach(row => {\n    const disciplina = String(row?.discNomeVc || '').trim();\n    const cod = String(row?.discCodVelhoVc || '').trim();\n    const horarios = Array.isArray(row?.horarios) ? row.horarios : [];\n    const professores = Array.isArray(row?.professores) ? row.professores : [];\n    const professor = String(professores[0]?.pessNomeVc || '').trim();\n\n    horarios.forEach(h => {\n      const parsed = parseHoraDescr(h?.horaDescrVc);\n      if (!parsed) return;\n\n      const slotWindow = SLOT_TIMES[parsed.turno][parsed.slot];\n      const sala = String(h?.ambienteNomeVc || '').trim();\n\n      slots.push({\n        dia: parsed.dia,\n        dayLabel: DAY_LABELS[parsed.dia],\n        turno: parsed.turno,\n        slot: parsed.slot,\n        inicio: slotWindow[0],\n        fim: slotWindow[1],\n        disciplina,\n        cod,\n        sala,\n        professor\n      });\n    });\n  });\n\n  slots.sort((a, b) => {\n    if (a.dia !== b.dia) return a.dia - b.dia;\n    if (a.turno !== b.turno) return TURN_ORDER[a.turno] - TURN_ORDER[b.turno];\n    return a.slot - b.slot;\n  });\n\n  return slots;\n}\n\nfunction mergeConsecutiveSlots(slots) {\n  const merged = [];\n\n  slots.forEach(slot => {\n    const prev = merged[merged.length - 1];\n    const canMerge =\n      prev &&\n      prev.dia === slot.dia &&\n      prev.turno === slot.turno &&\n      prev.disciplina === slot.disciplina &&\n      prev.cod === slot.cod &&\n      prev.slotEnd + 1 === slot.slot;\n\n    if (!canMerge) {\n      merged.push({\n        dia: slot.dia,\n        dayLabel: slot.dayLabel,\n        turno: slot.turno,\n        slotStart: slot.slot,\n        slotEnd: slot.slot,\n        inicio: slot.inicio,\n        fim: slot.fim,\n        disciplina: slot.disciplina,\n        cod: slot.cod,\n        salas: slot.sala ? [slot.sala] : [],\n        professor: slot.professor\n      });\n      return;\n    }\n\n    prev.slotEnd = slot.slot;\n    prev.fim = slot.fim;\n\n    if (slot.sala && !prev.salas.includes(slot.sala)) {\n      prev.salas.push(slot.sala);\n    }\n\n    if (!prev.professor && slot.professor) {\n      prev.professor = slot.professor;\n    }\n  });\n\n  return merged.map(item => ({\n    dia: item.dia,\n    dayLabel: item.dayLabel,\n    dayOffset: 0,\n    turno: item.turno,\n    slotStart: item.slotStart,\n    slotEnd: item.slotEnd,\n    inicio: item.inicio,\n    fim: item.fim,\n    disciplina: item.disciplina,\n    cod: item.cod,\n    sala: item.salas.join('/'),\n    professor: item.professor || ''\n  }));\n}\n\nfunction parseBoletimPayload(payload) {\n  const rows = Array.isArray(payload) ? payload : [];\n  const map = {};\n\n  rows.forEach(row => {\n    const cod = String(row?.discCodVelhoVc || '').trim();\n    if (!cod) return;\n\n    const faltas = safeNumber(row?.faltas, 0);\n    const aulasDadas = safeNumber(row?.aulasDadas, 0);\n    const aulasPrevistas = safeNumber(row?.aulasPrevistas, 0);\n    const limiteFaltas = aulasPrevistas > 0 ? Math.floor(aulasPrevistas * 0.25) : null;\n\n    map[cod] = {\n      faltas,\n      aulasDadas,\n      aulasPrevistas,\n      limiteFaltas,\n      faltasRestantes: Number.isFinite(limiteFaltas) ? limiteFaltas - faltas : null,\n      faltaPercent: aulasDadas > 0 ? Number(((100 * faltas) / aulasDadas).toFixed(1)) : null\n    };\n  });\n\n  return map;\n}\n\nfunction collectRowsFromItems(nodeName) {\n  return $items(nodeName).flatMap(item => {\n    const json = item?.json;\n    if (Array.isArray(json)) return json;\n    if (json && typeof json === 'object') return [json];\n    return [];\n  });\n}\n\nconst authPayload = $items('UTFPR Auth')[0]?.json || {};\nconst selectedCourse = $items('Select UTFPR Course')[0]?.json || {};\nconst horarioPayload = collectRowsFromItems('UTFPR Horario');\nconst boletimPayload = collectRowsFromItems('UTFPR Boletim');\n\nif (!selectedCourse.ok) {\n  const detail = String(selectedCourse.detail || authPayload.message || authPayload.error || '').trim();\n  return [{\n    json: {\n      ok: false,\n      error: selectedCourse.error || 'utfpr_data_unavailable',\n      detail,\n      todayClasses: [],\n      todayAbsences: [],\n      hint: null\n    }\n  }];\n}\n\nconst nowInfo = nowInfoInTz();\nconst slots = parseHorarioPayload(horarioPayload);\nconst mergedBlocks = mergeConsecutiveSlots(slots);\nconst absencesByCode = parseBoletimPayload(boletimPayload);\n\nconst blocks = mergedBlocks.map(block => {\n  const dayOffset = (block.dia - nowInfo.todayDayNumber + 7) % 7;\n  const absence = block.cod ? absencesByCode[block.cod] || null : null;\n  return {\n    ...block,\n    dayOffset,\n    absence\n  };\n});\n\nblocks.sort((a, b) => {\n  if (a.dayOffset !== b.dayOffset) return a.dayOffset - b.dayOffset;\n  if (a.turno !== b.turno) return TURN_ORDER[a.turno] - TURN_ORDER[b.turno];\n  return a.slotStart - b.slotStart;\n});\n\nconst todayClasses = blocks\n  .filter(block => block.dayOffset === 0)\n  .sort((a, b) => toMinutes(a.inicio) - toMinutes(b.inicio));\n\nconst todayClassSlots = todayClasses.reduce((acc, block) => acc + countSlotsInBlock(block), 0);\n\nconst nowOrUpcomingToday = todayClasses\n  .filter(block => toMinutes(block.fim) >= nowInfo.nowMinutes)\n  .sort((a, b) => toMinutes(a.inicio) - toMinutes(b.inicio));\n\nlet hint = null;\nconst prepMinutes = safeNumber($env.UTFPR_PREP_MINUTES, 45);\nconst commuteMinutes = safeNumber($env.UTFPR_COMMUTE_MINUTES, 20);\n\nif (nowOrUpcomingToday.length > 0) {\n  const first = nowOrUpcomingToday[0];\n  hint = {\n    when: 'hoje',\n    dayLabel: first.dayLabel,\n    classStartsAt: first.inicio,\n    arrumarAt: shiftHHmm(first.inicio, -prepMinutes),\n    sairAt: shiftHHmm(first.inicio, -commuteMinutes),\n    prepMinutes,\n    commuteMinutes\n  };\n}\n\nconst todayAbsences = [];\nconst seenTodayDisciplines = new Set();\n\ntodayClasses.forEach(block => {\n  const disciplineKey = String(block.cod || block.disciplina || '').trim();\n  if (!disciplineKey || seenTodayDisciplines.has(disciplineKey)) return;\n\n  seenTodayDisciplines.add(disciplineKey);\n  const abs = block.absence || {};\n\n  todayAbsences.push({\n    cod: String(block.cod || ''),\n    disciplina: String(block.disciplina || ''),\n    faltas: safeNumber(abs.faltas, 0),\n    aulasDadas: Number.isFinite(Number(abs.aulasDadas)) ? Number(abs.aulasDadas) : null,\n    aulasPrevistas: Number.isFinite(Number(abs.aulasPrevistas)) ? Number(abs.aulasPrevistas) : null,\n    limiteFaltas: Number.isFinite(Number(abs.limiteFaltas)) ? Number(abs.limiteFaltas) : null,\n    faltasRestantes: Number.isFinite(Number(abs.faltasRestantes)) ? Number(abs.faltasRestantes) : null,\n    faltaPercent: Number.isFinite(Number(abs.faltaPercent)) ? Number(abs.faltaPercent) : null\n  });\n});\n\ntodayAbsences.sort((a, b) => String(a.disciplina).localeCompare(String(b.disciplina), 'pt-BR'));\n\nconst absencesSummary = todayAbsences.reduce(\n  (acc, row) => {\n    acc.disciplines += 1;\n    acc.totalFaltas += safeNumber(row.faltas, 0);\n    if (Number.isFinite(row.limiteFaltas)) {\n      acc.totalLimiteFaltas += row.limiteFaltas;\n    }\n    if (Number.isFinite(row.faltasRestantes)) {\n      acc.totalRestantes += row.faltasRestantes;\n    }\n    return acc;\n  },\n  { disciplines: 0, totalFaltas: 0, totalLimiteFaltas: 0, totalRestantes: 0 }\n);\n\nreturn [{\n  json: {\n    ok: true,\n    source: 'utfpr-aulas',\n    generatedAtMs: Date.now(),\n    todayDayNumber: nowInfo.todayDayNumber,\n    student: {\n      nome: String(selectedCourse.nomeAluno || ''),\n      ra: String(selectedCourse.ra || ''),\n      curso: String(selectedCourse.cursoNome || ''),\n      codAluno: String(selectedCourse.codAluno || '')\n    },\n    totals: {\n      slots: slots.length,\n      blocks: blocks.length,\n      todayBlocks: todayClasses.length,\n      todaySlots: todayClassSlots,\n      todayDisciplines: seenTodayDisciplines.size,\n      today: todayClasses.length,\n      next: 0\n    },\n    hint,\n    absencesSummary,\n    todayAbsences,\n    todayClasses,\n    nextClasses: [],\n    allBlocks: todayClasses\n  }\n}];"
      },
      "id": "parse_aulas_data",
      "name": "Parse Aulas Data",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1200,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "// UTFPR Aulas \u2014 Format parsed classes into Telegram Markdown\n\nfunction esc(t) { return String(t).replace(/([_*`\\[\\]])/g, '\\\\$1'); }\n\nconst payload = $input.first().json || {};\n\nif (!payload.ok) {\n  const detail = String(payload.detail || '').trim();\n  let text = '*Aulas UTFPR*\\n\\n';\n  text += 'Nao foi possivel consultar o horario agora.\\n';\n  if (detail) {\n    text += `Detalhe: ${esc(detail)}\\n`;\n  }\n  text += '\\nConfira as variaveis UTFPR\\_PORTAL\\_USERNAME / UTFPR\\_PORTAL\\_PASSWORD e tente novamente.';\n  return [{ json: { text } }];\n}\n\nconst student = payload.student || {};\nconst totals = payload.totals || {};\nconst todayClasses = Array.isArray(payload.todayClasses) ? payload.todayClasses : [];\nconst todayAbsences = Array.isArray(payload.todayAbsences) ? payload.todayAbsences : [];\nconst hint = payload.hint || null;\n\nfunction countSlotsInBlock(block) {\n  const start = Number(block?.slotStart);\n  const end = Number(block?.slotEnd);\n  if (Number.isFinite(start) && Number.isFinite(end) && end >= start) {\n    return end - start + 1;\n  }\n  return 1;\n}\n\nfunction plural(value, one, many) {\n  return Number(value) === 1 ? one : many;\n}\n\nfunction renderBlocks(blocks) {\n  return blocks\n    .map(block => {\n      const cod = esc(block.cod || 'DISC');\n      const disciplina = esc(block.disciplina || 'Sem nome');\n      const inicio = esc(block.inicio || '--:--');\n      const fim = esc(block.fim || '--:--');\n\n      const slots = countSlotsInBlock(block);\n      const details = [`${slots} ${plural(slots, 'aula', 'aulas')}`];\n\n      if (block.sala) {\n        details.push(`Sala ${esc(block.sala)}`);\n      }\n\n      if (block.professor) {\n        details.push(`Prof. ${esc(block.professor)}`);\n      }\n\n      return `\u2022 *${inicio}-${fim}* | *[${cod}]* ${disciplina}\\n  ${details.join(' | ')}`;\n    })\n    .join('\\n\\n');\n}\n\nfunction formatAbsenceStatus(row) {\n  const faltas = Number.isFinite(Number(row.faltas)) ? Number(row.faltas) : 0;\n  const limite = Number.isFinite(Number(row.limiteFaltas)) ? Number(row.limiteFaltas) : null;\n  const restantes = Number.isFinite(Number(row.faltasRestantes)) ? Number(row.faltasRestantes) : null;\n\n  let status = Number.isFinite(limite)\n    ? `Faltas: *${faltas}/${limite}*`\n    : `Faltas: *${faltas}*`;\n\n  if (Number.isFinite(limite) && Number.isFinite(restantes)) {\n    status += restantes >= 0\n      ? ` | Restam *${restantes}*`\n      : ` | Excesso *${Math.abs(restantes)}*`;\n  }\n\n  return status;\n}\n\nfunction renderAbsences(rows) {\n  return rows\n    .map(row => {\n      const cod = esc(row.cod || 'DISC');\n      const disciplina = esc(row.disciplina || 'Sem nome');\n      return `\u2022 *[${cod}]* ${disciplina}\\n  ${formatAbsenceStatus(row)}`;\n    })\n    .join('\\n\\n');\n}\n\nconst todayBlocks = Number.isFinite(Number(totals.todayBlocks))\n  ? Number(totals.todayBlocks)\n  : todayClasses.length;\nconst todaySlots = Number.isFinite(Number(totals.todaySlots))\n  ? Number(totals.todaySlots)\n  : todayClasses.reduce((acc, block) => acc + countSlotsInBlock(block), 0);\nconst todayDisciplines = Number.isFinite(Number(totals.todayDisciplines))\n  ? Number(totals.todayDisciplines)\n  : new Set(todayClasses.map(block => String(block.cod || block.disciplina || '').trim()).filter(Boolean)).size;\n\nlet text = '*Aulas UTFPR*\\n\\n';\ntext += '*RESUMO*\\n';\ntext += `\u2022 Curso: *${esc(student.curso || 'N/A')}*\\n`;\ntext += `\u2022 Hoje: *${todaySlots}* ${plural(todaySlots, 'aula', 'aulas')} em *${todayBlocks}* ${plural(todayBlocks, 'bloco', 'blocos')}\\n`;\ntext += `\u2022 Disciplinas: *${todayDisciplines}*\\n`;\n\nif (hint && hint.when === 'hoje') {\n  text += `\u2022 Dica: arrumar *${esc(hint.arrumarAt)}* | sair *${esc(hint.sairAt)}*\\n`;\n}\n\ntext += '\\n*AULAS DE HOJE*\\n';\nif (todayClasses.length === 0) {\n  text += 'Nenhuma aula para hoje.\\n';\n} else {\n  text += `${renderBlocks(todayClasses)}\\n`;\n}\n\ntext += '\\n*FREQUENCIA*\\n';\nif (todayAbsences.length === 0) {\n  text += 'Sem dados de faltas para hoje.';\n} else {\n  text += renderAbsences(todayAbsences);\n}\n\nreturn [{ json: { text: text.trim() } }];"
      },
      "id": "format_aulas",
      "name": "Format Aulas",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1400,
        0
      ]
    },
    {
      "parameters": {
        "chatId": "={{ $env.TELEGRAM_CHAT_ID }}",
        "text": "={{ $json.text }}",
        "additionalFields": {
          "parse_mode": "Markdown",
          "appendAttribution": false
        }
      },
      "id": "telegram_aulas",
      "name": "Telegram",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.1,
      "position": [
        1600,
        0
      ],
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    }
  ],
  "connections": {
    "Execute Workflow Trigger": {
      "main": [
        [
          {
            "node": "UTFPR Auth",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "UTFPR Auth": {
      "main": [
        [
          {
            "node": "UTFPR Dados",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "UTFPR Dados": {
      "main": [
        [
          {
            "node": "Select UTFPR Course",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Select UTFPR Course": {
      "main": [
        [
          {
            "node": "UTFPR Horario",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "UTFPR Horario": {
      "main": [
        [
          {
            "node": "UTFPR Boletim",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "UTFPR Boletim": {
      "main": [
        [
          {
            "node": "Parse Aulas Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Aulas Data": {
      "main": [
        [
          {
            "node": "Format Aulas",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Aulas": {
      "main": [
        [
          {
            "node": "Telegram",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}

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

Aulas Sync. Uses executeWorkflowTrigger, httpRequest, telegram. Event-driven trigger; 9 nodes.

Source: https://github.com/webtraveler-br/servidor-assistente-com-n8n/blob/212c03ae8f676129a5fc3354b7c64e4876716fb1/workflows/aulas-sync.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

This workflow provides a complete solution for handling Telegram Stars payments, invoicing and refunds using n8n. It automates the process of sending invoices, managing pre-checkout approvals, recordi

HTTP Request, Execute Workflow Trigger, Google Sheets +2
Slack & Telegram

VIVID v5.0 — Chapter Sub-workflow. Uses executeWorkflowTrigger, executeCommand, itemLists, httpRequest. Event-driven trigger; 21 nodes.

Execute Workflow Trigger, Execute Command, Item Lists +2
Slack & Telegram

[HUB] Жора Action. Uses executeWorkflowTrigger, supabase, telegram, httpRequest. Event-driven trigger; 19 nodes.

Execute Workflow Trigger, Supabase, Telegram +1
Slack & Telegram

02_Audio_Extractor. Uses executeWorkflowTrigger, telegram, httpRequest. Event-driven trigger; 13 nodes.

Execute Workflow Trigger, Telegram, HTTP Request
Slack & Telegram

[HUB] Жора Capture. Uses executeWorkflowTrigger, httpRequest, supabase, telegram. Event-driven trigger; 8 nodes.

Execute Workflow Trigger, HTTP Request, Supabase +1