AutomationFlowsAI & RAG › Jira Copy

Jira Copy

JIRA copy. Uses httpRequest, jira, googleGemini. Event-driven trigger; 11 nodes.

Event trigger★★★★☆ complexityAI-powered11 nodesHTTP RequestJiraGoogle Gemini
AI & RAG Trigger: Event Nodes: 11 Complexity: ★★★★☆ AI nodes: yes Added:

This workflow follows the Googlegemini → 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": "JIRA copy",
  "nodes": [
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        176,
        -64
      ],
      "id": "d06c7c09-28e8-4706-8585-0c205b5dfc9e",
      "name": "When clicking 'Execute workflow'"
    },
    {
      "parameters": {
        "url": "https://gupy-io.atlassian.net/rest/agile/1.0/board/3128/sprint",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "jiraSoftwareCloudApi",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "state",
              "value": "closed"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        368,
        -64
      ],
      "id": "d53b6ef6-4a0a-4052-942b-ba92156199a3",
      "name": "1. Buscar Sprints Fechadas",
      "credentials": {
        "jiraSoftwareCloudApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const vals = $json.values || [];\nconst d = x => new Date(x).toISOString().slice(0,10);\nconst today = new Date().toISOString().slice(0,10);\nlet s = vals.find(v => (v.completeDate && d(v.completeDate) === today) || (v.endDate && d(v.endDate) === today));\nif (!s) s = vals.sort((a, b) => new Date(b.endDate) - new Date(a.endDate))[0];\nif (!s) throw new Error('Nenhuma sprint fechada encontrada.');\nreturn [{json: {sprintId: s.id, sprintName: s.name, sprintStartDate: s.startDate, sprintEndDate: s.endDate, sprintCompleteDate: s.completeDate || s.endDate, sprintGoal: s.goal || ''}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        576,
        -64
      ],
      "id": "6e5cb78e-7870-4593-a0a1-093abb4c436a",
      "name": "2. Selecionar Sprint"
    },
    {
      "parameters": {
        "operation": "getAll",
        "returnAll": true,
        "options": {
          "jql": "=Sprint = {{ $json.sprintId }}"
        }
      },
      "type": "n8n-nodes-base.jira",
      "typeVersion": 1,
      "position": [
        768,
        -64
      ],
      "id": "6507c48a-acf1-4d16-8765-38f501fde9ed",
      "name": "3. Buscar Issues da Sprint (Jira)",
      "credentials": {
        "jiraSoftwareCloudApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Busca dados da sprint do node \"2. Selecionar Sprint\"\nconst sprintNode = $('2. Selecionar Sprint');\nconst sprintData = {\n  sprintId: sprintNode.item.json.sprintId,\n  sprintName: sprintNode.item.json.sprintName,\n  sprintStartDate: sprintNode.item.json.sprintStartDate,\n  sprintEndDate: sprintNode.item.json.sprintEndDate,\n  sprintCompleteDate: sprintNode.item.json.sprintCompleteDate,\n  sprintGoal: sprintNode.item.json.sprintGoal\n};\n\n// Retorna um item por issue key com dados da sprint\nreturn $input.all().map(item => ({\n  json: {\n    issueKey: item.json.key,\n    ...sprintData\n  }\n}));"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        976,
        -64
      ],
      "id": "4953b222-ae34-4e48-890a-8acb84267553",
      "name": "4. Preparar Lista de Issues"
    },
    {
      "parameters": {
        "url": "=https://gupy-io.atlassian.net/rest/api/3/issue/{{$json.issueKey}}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "jiraSoftwareCloudApi",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "expand",
              "value": "changelog,renderedFields"
            },
            {
              "name": "fields",
              "value": "summary,description,status,project,assignee,reporter,labels,created,updated,resolutiondate,issuetype,priority,parent,subtasks,duedate,comment"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1184,
        -64
      ],
      "id": "8595f398-6374-4d4d-ab73-032f4d9d7135",
      "name": "5. Buscar Detalhes Completos",
      "credentials": {
        "jiraSoftwareCloudApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Busca dados da sprint do node \"2. Selecionar Sprint\"\nconst sprintNode = $('2. Selecionar Sprint');\n\nif (!sprintNode || !sprintNode.item || !sprintNode.item.json) {\n  throw new Error('N\u00e3o foi poss\u00edvel acessar dados da sprint do node \"2. Selecionar Sprint\"');\n}\n\nconst sprint = sprintNode.item.json;\nconst sprintData = {\n  sprintId: sprint.sprintId,\n  sprintName: sprint.sprintName,\n  sprintStartDate: sprint.sprintStartDate,\n  sprintEndDate: sprint.sprintEndDate,\n  sprintCompleteDate: sprint.sprintCompleteDate,\n  sprintGoal: sprint.sprintGoal || ''\n};\n\n// Verifica se temos os dados essenciais\nif (!sprintData.sprintId || !sprintData.sprintName) {\n  throw new Error('Dados da sprint incompletos: ' + JSON.stringify(sprintData));\n}\n\n// Pega todas as issues do step anterior\nconst allItems = $input.all();\nconst issues = allItems.map(item => item.json).filter(item => item.key);\n\nif (issues.length === 0) {\n  throw new Error('Nenhuma issue encontrada no input');\n}\n\n// Retorna um \u00fanico objeto com dados da sprint + array de issues\nconst result = {\n  ...sprintData,\n  total_issues: issues.length,\n  issues: issues\n};\n\n// Log para debug\nconsole.log('Sprint ID:', result.sprintId);\nconsole.log('Total Issues:', result.total_issues);\n\nreturn [{json: result}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1376,
        -64
      ],
      "id": "7a675d89-fde2-4d84-9a01-2fb1dd378df6",
      "name": "6. Consolidar Issues"
    },
    {
      "parameters": {
        "jsCode": "// ============================================\n// HELPERS DE TEXTO E ARRAYS\n// ============================================\n\n// Converte descri\u00e7\u00e3o ADF para texto simples\nfunction adfToText(desc) {\n  if (!desc) return '';\n  if (typeof desc === 'string') return desc;\n  try {\n    const walk = (n) => {\n      if (!n) return '';\n      if (n.type === 'text') return n.text || '';\n      if (Array.isArray(n.content)) {\n        return n.content.map(walk).join(n.type === 'paragraph' ? '\\n' : '');\n      }\n      return '';\n    };\n    return walk(desc).trim();\n  } catch {\n    return '';\n  }\n}\n\n// Estat\u00edsticas simples (percentis sobre valores num\u00e9ricos)\nfunction stats(arr) {\n  const v = arr.filter(x => Number.isFinite(x)).sort((a, b) => a - b);\n  if (!v.length) return { p50: null, p90: null, p95: null, avg: null, min: null, max: null };\n  const mid = Math.floor(v.length / 2);\n  const p50 = v.length % 2 ? v[mid] : (v[mid - 1] + v[mid]) / 2;\n  const p90 = v[Math.max(0, Math.ceil(v.length * 0.9) - 1)];\n  const p95 = v[Math.max(0, Math.ceil(v.length * 0.95) - 1)];\n  const avg = v.reduce((a, b) => a + b, 0) / v.length;\n  return { p50, p90, p95, avg, min: v[0], max: v[v.length - 1] };\n}\n\n// Normaliza strings (acentos, caixa, espa\u00e7os)\nfunction norm(s) {\n  return (s || '')\n    .toString()\n    .normalize('NFD')\n    .replace(/\\p{Diacritic}/gu, '')\n    .toLowerCase()\n    .trim();\n}\n\n// ============================================\n// DIAS \u00daTEIS (exclui s\u00e1b/dom) \u2014 retorna dias decimais\n// ============================================\n\n// avan\u00e7a para o pr\u00f3ximo dia \u00fatil \u00e0s 00:00 se cair em fim de semana\nfunction nextBusinessStart(d) {\n  const x = new Date(d);\n  while (x.getDay() === 0 || x.getDay() === 6) { // 0=Dom, 6=S\u00e1b\n    x.setDate(x.getDate() + 1);\n    x.setHours(0,0,0,0);\n  }\n  return x;\n}\n// retrocede para o \u00faltimo dia \u00fatil \u00e0s 23:59:59.999 se cair em fim de semana\nfunction prevBusinessEnd(d) {\n  const x = new Date(d);\n  while (x.getDay() === 0 || x.getDay() === 6) {\n    x.setDate(x.getDate() - 1);\n    x.setHours(23,59,59,999);\n  }\n  return x;\n}\n\n// diferen\u00e7a em dias \u00fateis (aceita quaisquer hor\u00e1rios; assume jornada 24h para simplificar a fra\u00e7\u00e3o do dia)\nfunction businessDiffDays(a, b) {\n  if (!a || !b) return null;\n  let start = new Date(a);\n  let end = new Date(b);\n  if (end <= start) return 0;\n\n  start = nextBusinessStart(start);\n  end = prevBusinessEnd(end);\n  if (end <= start) return 0;\n\n  // conta dias inteiros \u00fateis\n  let days = 0;\n  const cur = new Date(start);\n  cur.setHours(0,0,0,0);\n  end.setHours(23,59,59,999);\n\n  while (cur <= end) {\n    const dow = cur.getDay();\n    if (dow !== 0 && dow !== 6) days += 1;\n    cur.setDate(cur.getDate() + 1);\n  }\n\n  // Ajuste fino de fra\u00e7\u00f5es do primeiro/\u00faltimo dia\n  // (mantemos simples: se inicio/fim no meio do dia \u00fatil, aproximamos por propor\u00e7\u00e3o linear 24h)\n  // Primeiro dia\n  const firstDayStart = new Date(start); firstDayStart.setHours(0,0,0,0);\n  const firstDayEnd = new Date(start); firstDayEnd.setHours(23,59,59,999);\n  if (firstDayStart.getDay() !== 0 && firstDayStart.getDay() !== 6) {\n    const frac = (start - firstDayStart) / (24*3600*1000);\n    days -= frac;\n  }\n\n  // \u00daltimo dia\n  const lastDayStart = new Date(end); lastDayStart.setHours(0,0,0,0);\n  const lastDayEnd = new Date(end); lastDayEnd.setHours(23,59,59,999);\n  if (lastDayStart.getDay() !== 0 && lastDayStart.getDay() !== 6) {\n    const frac = (lastDayEnd - end) / (24*3600*1000);\n    days -= frac;\n  }\n\n  return Math.max(0, Number(days));\n}\n\n// ============================================\n// DEFINI\u00c7\u00c3O DO TEU FLUXO (ajuste livre se renomear colunas)\n// ============================================\n\n// Done (categorias/status de conclus\u00e3o)\nconst DONE_STATUSES = [\n  'done','concluido','conclu\u00eddo','fechado','closed','itens concluidos','itens conclu\u00eddos'\n].map(norm);\n\n// Status de prepara\u00e7\u00e3o / discovery / backlog\nconst PREP_STATUSES = [\n  'to do','priorizado','tarefas pendentes',\n  'ideias e insights',\n  'exploracao e analise','explora\u00e7\u00e3o e an\u00e1lise',\n  'prototipagem e validacao','prototipagem e valida\u00e7\u00e3o',\n  'design review',\n  'refinamento tecnico','refinamento t\u00e9cnico',\n  'pronto para desenvolvimento','pronto para desenvolver'\n].map(norm);\n\n// Status de trabalho ativo (delivery)\nconst WORK_STATUSES = [\n  'in progress','em andamento',\n  'code review',\n  'validacao','valida\u00e7\u00e3o',\n  'aceitacao','aceita\u00e7\u00e3o',\n  'pronto para deploy'\n].map(norm);\n\n// Mapeamento de status \u2192 fase macro\nfunction phaseOfStatus(sNorm) {\n  if (['ideias e insights','exploracao e analise','explora\u00e7\u00e3o e an\u00e1lise','prototipagem e validacao','prototipagem e valida\u00e7\u00e3o','design review','refinamento tecnico','refinamento t\u00e9cnico'].includes(sNorm)) {\n    return 'discovery';\n  }\n  if (['priorizado','tarefas pendentes','pronto para desenvolvimento','pronto para desenvolver','to do'].includes(sNorm)) {\n    return 'backlog';\n  }\n  if (['in progress','em andamento'].includes(sNorm)) return 'build';\n  if (['code review','design review'].includes(sNorm)) return 'review';\n  if (['validacao','valida\u00e7\u00e3o','aceitacao','aceita\u00e7\u00e3o'].includes(sNorm)) return 'validation';\n  if (['pronto para deploy'].includes(sNorm)) return 'deploy';\n  if (DONE_STATUSES.includes(sNorm)) return 'done';\n  return 'other';\n}\n\n// ============================================\n// HELPERS COM CHANGELOG\n// ============================================\n\n// Extrai todas transi\u00e7\u00f5es de status ordenadas\nfunction extractStatusEvents(changelog) {\n  if (!changelog?.histories) return [];\n  const events = [];\n  for (const h of changelog.histories) {\n    const at = new Date(h.created);\n    for (const it of h.items || []) {\n      if (it.field === 'status') {\n        events.push({\n          at,\n          from: norm(it.fromString || it.from),\n          to: norm(it.toString || it.to)\n        });\n      }\n    }\n  }\n  events.sort((a,b) => a.at - b.at);\n  return events;\n}\n\n// Primeiro momento em que a issue entrou em algum status do conjunto alvo\nfunction firstTransitionToNormalized(changelog, targetSet) {\n  const events = extractStatusEvents(changelog);\n  const hit = events.find(e => targetSet.includes(e.to));\n  return hit ? hit.at : null;\n}\n\n// \"In\u00edcio de trabalho\" robusto: 1) primeira ida para WORK; sen\u00e3o 2) primeira sa\u00edda de PREP para fora de PREP\nfunction firstWorkStart(changelog) {\n  const events = extractStatusEvents(changelog);\n  if (!events.length) return null;\n  const toWork = events.find(e => WORK_STATUSES.includes(e.to));\n  if (toWork) return toWork.at;\n  const exitPrep = events.find(e => PREP_STATUSES.includes(e.from) && !PREP_STATUSES.includes(e.to));\n  return exitPrep ? exitPrep.at : null;\n}\n\n// Conta reaberturas (saiu de Done para outro status)\nfunction countReopened(changelog) {\n  const events = extractStatusEvents(changelog);\n  let c = 0;\n  for (const e of events) {\n    if (DONE_STATUSES.includes(e.from) && !DONE_STATUSES.includes(e.to)) c++;\n  }\n  return c;\n}\n\n// Tempo total (horas) em um status espec\u00edfico (calend\u00e1rio)\nfunction totalTimeInStatusHours(changelog, statusName, now) {\n  const sNorm = norm(statusName);\n  const events = extractStatusEvents(changelog);\n  if (!events.length) return 0;\n\n  let totalMs = 0;\n  let inStatus = false;\n  let enteredAt = null;\n\n  for (const e of events) {\n    if (!inStatus && e.to === sNorm) {\n      inStatus = true; enteredAt = e.at;\n    } else if (inStatus && e.from === sNorm) {\n      totalMs += (e.at - enteredAt);\n      inStatus = false; enteredAt = null;\n    }\n  }\n  if (inStatus && enteredAt) totalMs += (now - enteredAt);\n  return totalMs / 3600000;\n}\n\n// Tempo por fase (dias \u00fateis). Soma os intervalos entre eventos por status \u2192 fase\nfunction phaseTimesBusinessDays(changelog, fields, now) {\n  const events = extractStatusEvents(changelog);\n  const out = { discovery: 0, backlog: 0, build: 0, review: 0, validation: 0, deploy: 0, done: 0 };\n\n  if (!events.length) return out;\n\n  // Tentativa de status \"inicial\": usa from do primeiro evento; se nulo, usa status atual do fields como ponto de partida\n  let curStatus = events[0].from || norm(fields?.status?.name);\n  let curAt = new Date(fields?.created || events[0].at);\n\n  for (const e of events) {\n    const phase = phaseOfStatus(curStatus);\n    const segEnd = e.at;\n    const days = businessDiffDays(curAt, segEnd);\n    if (Number.isFinite(days)) {\n      if (!out[phase]) out[phase] = 0;\n      out[phase] += days;\n    }\n    // avan\u00e7a\n    curStatus = e.to;\n    curAt = e.at;\n  }\n\n  // \u00faltimo trecho at\u00e9 \"agora\"\n  const lastPhase = phaseOfStatus(curStatus);\n  const days = businessDiffDays(curAt, now);\n  if (Number.isFinite(days)) {\n    if (!out[lastPhase]) out[lastPhase] = 0;\n    out[lastPhase] += days;\n  }\n\n  // arredonda com 2 casas\n  for (const k of Object.keys(out)) out[k] = Number(out[k].toFixed(2));\n  return out;\n}\n\n// Detecta quando a issue entrou na sprint (via altera\u00e7\u00e3o no campo Sprint)\nfunction getSprintEntryDate(changelog, sprintId) {\n  if (!changelog?.histories) return null;\n  for (const h of changelog.histories) {\n    for (const it of h.items || []) {\n      if (it.field === 'Sprint') {\n        const raw = (it.to || it.toString || '') + '';\n        if (raw.includes(String(sprintId))) return new Date(h.created);\n      }\n    }\n  }\n  return null;\n}\n\n// Fluxo bruto de status (para debug/insights)\nfunction getStatusFlow(changelog) {\n  const events = extractStatusEvents(changelog);\n  return events.map(e => ({\n    from: e.from || null,\n    to: e.to || null,\n    at: e.at.toISOString()\n  }));\n}\n\n// ============================================\n// PROCESSAMENTO\n// ============================================\n\nconst input = $json;\nconst sprintId = input.sprintId;\nconst sprintName = input.sprintName;\nconst sprintStart = input.sprintStartDate ? new Date(input.sprintStartDate) : null;\nconst sprintEnd = input.sprintEndDate ? new Date(input.sprintEndDate) : null;\nconst sprintComplete = input.sprintCompleteDate ? new Date(input.sprintCompleteDate) : sprintEnd;\nconst issues = Array.isArray(input.issues) ? input.issues : [];\n\nconst now = new Date();\n\nconst bugTypes = ['bug','defeito'].map(norm);\nconst blockedStatusName = 'Blocked'; // ajuste se usa outro r\u00f3tulo\n\n// ===== Enriquecimento por issue =====\nconst enrichedIssues = issues.map(issue => {\n  const fields = issue.fields || {};\n  const changelog = issue.changelog || {};\n\n  // Datas b\u00e1sicas\n  const created = fields.created ? new Date(fields.created) : null;\n  const resolved = fields.resolutiondate ? new Date(fields.resolutiondate) : null;\n  const updated = fields.updated ? new Date(fields.updated) : null;\n\n  // Status / tipo\n  const statusName = fields.status?.name || null;\n  const statusNorm = norm(statusName);\n  const statusCategory = fields.status?.statusCategory?.name || null;\n  const isDone = DONE_STATUSES.includes(statusNorm) || norm(statusCategory) === 'done';\n  const issueType = fields.issuetype?.name || 'Unknown';\n  const isBug = bugTypes.includes(norm(issueType));\n  const priority = fields.priority?.name || 'None';\n\n  // Pessoas\n  const assignee = fields.assignee?.displayName || 'Unassigned';\n  const reporter = fields.reporter?.displayName || 'Unknown';\n\n  // Transi\u00e7\u00f5es chave\n  const firstDone = firstTransitionToNormalized(changelog, DONE_STATUSES);\n  const workStartAt = firstWorkStart(changelog);\n\n  // M\u00e9tricas de tempo (DIAS \u00daTEIS)\n  const leadTimeDays  = (created && resolved) ? businessDiffDays(created, resolved) : null;\n  const cycleTimeDays = (workStartAt && resolved) ? businessDiffDays(workStartAt, resolved) : null;\n  const waitTimeDays  = (created && workStartAt) ? businessDiffDays(created, workStartAt) : null;\n\n  // Aging (abertas): desde \u00faltima mudan\u00e7a at\u00e9 agora, em DIAS \u00daTEIS\n  let lastChangeDate = updated || created;\n  if (changelog?.histories?.length > 0) {\n    const lastHistory = changelog.histories[changelog.histories.length - 1];\n    const lastHistoryDate = new Date(lastHistory.created);\n    if (lastHistoryDate > lastChangeDate) lastChangeDate = lastHistoryDate;\n  }\n  const agingDays = !isDone && lastChangeDate ? businessDiffDays(lastChangeDate, now) : 0;\n\n  // Sprint\n  const enteredSprintAt = getSprintEntryDate(changelog, sprintId);\n  const isLateEntry = !!(enteredSprintAt && sprintStart && (enteredSprintAt > sprintStart));\n  const wasInSprintAtStart = (!isLateEntry) && (!!sprintStart) && ((created && created < sprintStart) || (enteredSprintAt && enteredSprintAt < sprintStart));\n\n  // Qualidade\n  const reopenedCount = countReopened(changelog);\n  const hasRework = reopenedCount > 0;\n  const blockedTimeHours = blockedStatusName ? totalTimeInStatusHours(changelog, blockedStatusName, now) : 0;\n\n  // Fluxo\n  const statusFlow = getStatusFlow(changelog);\n  const statusChanges = statusFlow.length;\n\n  // Descri\u00e7\u00e3o\n  const description = adfToText(fields.description);\n  const hasDescription = description.length > 10;\n\n  // Subtasks\n  const hasSubtasks = Array.isArray(fields.subtasks) && fields.subtasks.length > 0;\n  const subtasksCount = hasSubtasks ? fields.subtasks.length : 0;\n\n  // Labels\n  const labels = Array.isArray(fields.labels) ? fields.labels : [];\n\n  // Tempos por fase (dias \u00fateis)\n  const phaseTimesDays = phaseTimesBusinessDays(changelog, fields, now);\n\n  // Primeira \"ida para trabalho ativo\" (para debug/insights)\n  const firstInProgress = firstTransitionToNormalized(changelog, WORK_STATUSES);\n\n  return {\n    key: issue.key,\n    summary: fields.summary || '',\n    description_text: description,\n    has_description: hasDescription,\n\n    status_current: statusName,\n    status_category: statusCategory,\n    is_done: isDone,\n    issue_type: issueType,\n    is_bug: isBug,\n    priority: priority,\n\n    assignee,\n    reporter,\n\n    created: created?.toISOString() || null,\n    resolved: resolved?.toISOString() || null,\n    updated: updated?.toISOString() || null,\n    first_in_progress: firstInProgress?.toISOString() || null,\n    first_done: firstDone?.toISOString() || null,\n\n    // tempos (DIAS \u00daTEIS)\n    lead_time_days: leadTimeDays !== null ? Number(leadTimeDays.toFixed(2)) : null,\n    cycle_time_days: cycleTimeDays !== null ? Number(cycleTimeDays.toFixed(2)) : null,\n    wait_time_days:  waitTimeDays  !== null ? Number(waitTimeDays.toFixed(2))  : null,\n    aging_days: Number((agingDays || 0).toFixed(2)),\n\n    // Sprint\n    entered_sprint_at: enteredSprintAt?.toISOString() || null,\n    is_late_entry: isLateEntry,\n    was_in_sprint_at_start: !!wasInSprintAtStart,\n\n    // Qualidade\n    reopened_count: reopenedCount,\n    has_rework: hasRework,\n    blocked_time_hours: blockedTimeHours ? Number(blockedTimeHours.toFixed(2)) : 0,\n\n    // Fluxo\n    status_changes: statusChanges,\n    status_flow: statusFlow,\n\n    // Outros\n    has_subtasks: hasSubtasks,\n    subtasks_count: subtasksCount,\n    labels,\n\n    // Fases\n    phase_times_days: {\n      discovery: phaseTimesDays.discovery || 0,\n      backlog: phaseTimesDays.backlog || 0,\n      build: phaseTimesDays.build || 0,\n      review: phaseTimesDays.review || 0,\n      validation: phaseTimesDays.validation || 0,\n      deploy: phaseTimesDays.deploy || 0\n    }\n  };\n});\n\n// ============================================\n// M\u00c9TRICAS AGREGADAS\n// ============================================\n\nconst doneIssues = enrichedIssues.filter(i => i.is_done);\nconst openIssues = enrichedIssues.filter(i => !i.is_done);\nconst bugsTotal = enrichedIssues.filter(i => i.is_bug);\nconst bugsDone = doneIssues.filter(i => i.is_bug);\nconst bugsOpen = openIssues.filter(i => i.is_bug);\n\n// Escopo\nconst scopePlanned = enrichedIssues.filter(i => i.was_in_sprint_at_start).length;\nconst scopeAdded = enrichedIssues.filter(i => i.is_late_entry).length;\nconst scopeCompleted = doneIssues.length;\nconst scopeIncomplete = openIssues.length;\nconst commitmentDeliveryRatio = scopePlanned > 0 ? (scopeCompleted / scopePlanned) : null;\nconst scopeCreepRatio = enrichedIssues.length > 0 ? (scopeAdded / enrichedIssues.length) : 0;\n\n// Fluxo (dias \u00fateis)\nconst leadTimeStats = stats(doneIssues.map(i => i.lead_time_days));\nconst cycleTimeStats = stats(doneIssues.map(i => i.cycle_time_days));\nconst waitTimeStats = stats(doneIssues.map(i => i.wait_time_days));\n\n// WIP e aging (dias \u00fateis)\nconst wipAgingStats = stats(openIssues.map(i => i.aging_days));\nconst currentWip = openIssues.length;\n\n// Throughput e dura\u00e7\u00e3o de sprint (calend\u00e1rio)\nconst sprintDurationDays = (sprintStart && sprintComplete) ? (sprintComplete - sprintStart) / 86400000 : null;\nconst throughput = doneIssues.length;\nconst throughputPerDay = sprintDurationDays > 0 ? (throughput / sprintDurationDays) : null;\n\n// Qualidade\nconst reworkIssues = enrichedIssues.filter(i => i.has_rework).length;\nconst reworkRatio = doneIssues.length > 0 ? (reworkIssues / doneIssues.length) : 0;\nconst bugRatio = enrichedIssues.length > 0 ? (bugsTotal.length / enrichedIssues.length) : 0;\nconst blockedTotalHours = enrichedIssues.reduce((sum, i) => sum + (i.blocked_time_hours || 0), 0);\nconst issuesWithoutDescription = enrichedIssues.filter(i => !i.has_description).length;\n\n// Balance por pessoa\nconst loadBalance = {};\nfor (const it of enrichedIssues) {\n  const who = it.assignee || 'Unassigned';\n  if (!loadBalance[who]) loadBalance[who] = { total: 0, done: 0, open: 0, bugs: 0 };\n  loadBalance[who].total++;\n  if (it.is_done) loadBalance[who].done++; else loadBalance[who].open++;\n  if (it.is_bug) loadBalance[who].bugs++;\n}\n\n// Distribui\u00e7\u00e3o por tipo / prioridade\nconst issueTypesDist = {};\nfor (const it of enrichedIssues) {\n  const t = it.issue_type || 'Unknown';\n  if (!issueTypesDist[t]) issueTypesDist[t] = { total: 0, done: 0 };\n  issueTypesDist[t].total++;\n  if (it.is_done) issueTypesDist[t].done++;\n}\nconst priorityDist = {};\nfor (const it of enrichedIssues) {\n  const p = it.priority || 'None';\n  if (!priorityDist[p]) priorityDist[p] = { total: 0, done: 0 };\n  priorityDist[p].total++;\n  if (it.is_done) priorityDist[p].done++;\n}\n\n// Top aging / status changes\nconst topAgingIssues = [...openIssues]\n  .sort((a,b) => (b.aging_days||0) - (a.aging_days||0))\n  .slice(0, 10)\n  .map(i => ({\n    key: i.key, summary: i.summary, assignee: i.assignee, status: i.status_current,\n    aging_days: i.aging_days, priority: i.priority, issue_type: i.issue_type\n  }));\n\nconst topStatusChanges = [...enrichedIssues]\n  .sort((a,b) => (b.status_changes||0) - (a.status_changes||0))\n  .slice(0, 10)\n  .map(i => ({\n    key: i.key, summary: i.summary, status_changes: i.status_changes,\n    is_done: i.is_done, reopened_count: i.reopened_count\n  }));\n\n// Previsibilidade (coef. varia\u00e7\u00e3o do cycle time em DIAS \u00daTEIS)\nconst cycleAvg = cycleTimeStats.avg;\nlet cycleCV = null, predictabilityScore = null;\nif (cycleAvg && cycleAvg > 0 && doneIssues.length > 1) {\n  const variance = doneIssues.reduce((acc, i) => {\n    if (!Number.isFinite(i.cycle_time_days)) return acc;\n    const d = i.cycle_time_days - cycleAvg;\n    return acc + d*d;\n  }, 0) / (doneIssues.length - 1);\n  const stdev = Math.sqrt(variance);\n  cycleCV = stdev / cycleAvg;\n  predictabilityScore = Math.max(0, Math.min(100, 100 * (1 - cycleCV)));\n}\n\n// ============================================\n// RESULTADO\n// ============================================\n\nreturn [{\n  json: {\n    sprint: {\n      id: sprintId,\n      name: sprintName,\n      start_date: sprintStart?.toISOString() || null,\n      end_date: sprintEnd?.toISOString() || null,\n      complete_date: sprintComplete?.toISOString() || null,\n      duration_days: sprintDurationDays ? Number(sprintDurationDays.toFixed(1)) : null,\n      goal: input.sprintGoal || ''\n    },\n    metrics: {\n      scope: {\n        total: enrichedIssues.length,\n        planned: scopePlanned,\n        added: scopeAdded,\n        completed: scopeCompleted,\n        incomplete: scopeIncomplete,\n        commitment_delivery_ratio: commitmentDeliveryRatio ? Number(commitmentDeliveryRatio.toFixed(2)) : null,\n        scope_creep_ratio: Number(scopeCreepRatio.toFixed(2))\n      },\n      flow: {\n        throughput: throughput,\n        throughput_per_day: throughputPerDay ? Number(throughputPerDay.toFixed(2)) : null,\n        // DIAS \u00daTEIS\n        lead_time: {\n          p50: leadTimeStats.p50 ? Number(leadTimeStats.p50.toFixed(2)) : null,\n          p90: leadTimeStats.p90 ? Number(leadTimeStats.p90.toFixed(2)) : null,\n          p95: leadTimeStats.p95 ? Number(leadTimeStats.p95.toFixed(2)) : null,\n          avg: leadTimeStats.avg ? Number(leadTimeStats.avg.toFixed(2)) : null,\n          min: leadTimeStats.min ? Number(leadTimeStats.min.toFixed(2)) : null,\n          max: leadTimeStats.max ? Number(leadTimeStats.max.toFixed(2)) : null\n        },\n        cycle_time: {\n          p50: cycleTimeStats.p50 ? Number(cycleTimeStats.p50.toFixed(2)) : null,\n          p90: cycleTimeStats.p90 ? Number(cycleTimeStats.p90.toFixed(2)) : null,\n          p95: cycleTimeStats.p95 ? Number(cycleTimeStats.p95.toFixed(2)) : null,\n          avg: cycleTimeStats.avg ? Number(cycleTimeStats.avg.toFixed(2)) : null,\n          min: cycleTimeStats.min ? Number(cycleTimeStats.min.toFixed(2)) : null,\n          max: cycleTimeStats.max ? Number(cycleTimeStats.max.toFixed(2)) : null\n        },\n        wait_time: {\n          p50: waitTimeStats.p50 ? Number(waitTimeStats.p50.toFixed(2)) : null,\n          p90: waitTimeStats.p90 ? Number(waitTimeStats.p90.toFixed(2)) : null,\n          avg: waitTimeStats.avg ? Number(waitTimeStats.avg.toFixed(2)) : null\n        }\n      },\n      wip: {\n        current: currentWip,\n        aging_p50: wipAgingStats.p50 ? Number(wipAgingStats.p50.toFixed(2)) : null,\n        aging_p90: wipAgingStats.p90 ? Number(wipAgingStats.p90.toFixed(2)) : null,\n        aging_max: wipAgingStats.max ? Number(wipAgingStats.max.toFixed(2)) : null,\n        by_phase: {\n          discovery: Number(openIssues.reduce((s,i)=>s+(i.phase_times_days?.discovery||0),0).toFixed(0)),\n          backlog:   Number(openIssues.reduce((s,i)=>s+(i.phase_times_days?.backlog||0),0).toFixed(0)),\n          build:     Number(openIssues.reduce((s,i)=>s+(i.phase_times_days?.build||0),0).toFixed(0)),\n          review:    Number(openIssues.reduce((s,i)=>s+(i.phase_times_days?.review||0),0).toFixed(0)),\n          validation:Number(openIssues.reduce((s,i)=>s+(i.phase_times_days?.validation||0),0).toFixed(0)),\n          deploy:    Number(openIssues.reduce((s,i)=>s+(i.phase_times_days?.deploy||0),0).toFixed(0)),\n        }\n      },\n      quality: {\n        bugs_total: bugsTotal.length,\n        bugs_done: bugsDone.length,\n        bugs_open: bugsOpen.length,\n        bug_ratio: Number(bugRatio.toFixed(2)),\n        rework_issues: reworkIssues,\n        rework_ratio: Number(reworkRatio.toFixed(2)),\n        blocked_total_hours: Number(blockedTotalHours.toFixed(2)),\n        issues_without_description: issuesWithoutDescription\n      },\n      predictability: {\n        score: predictabilityScore !== null ? Number(predictabilityScore.toFixed(1)) : null,\n        coefficient_variation: cycleCV !== null ? Number(cycleCV.toFixed(2)) : null,\n        note: \"Score 0-100: quanto maior, mais previs\u00edvel (cycle time em dias \u00fateis)\"\n      },\n      distributions: {\n        by_assignee: loadBalance,\n        by_issue_type: issueTypesDist,\n        by_priority: priorityDist\n      }\n    },\n    insights: {\n      top_aging_issues: topAgingIssues,\n      top_status_changes: topStatusChanges\n    },\n    issues_detail: enrichedIssues\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1584,
        -64
      ],
      "id": "d16f1091-d1ef-4193-bfe0-e2fa23c3591b",
      "name": "7. Calcular M\u00e9tricas"
    },
    {
      "parameters": {
        "modelId": {
          "__rl": true,
          "value": "models/gemini-2.5-pro",
          "mode": "list",
          "cachedResultName": "models/gemini-2.5-pro"
        },
        "messages": {
          "values": [
            {
              "content": "=Voc\u00ea \u00e9 um gerente de engenharia experiente em m\u00e9tricas de fluxo e Kanban.\n\nAnalise os dados desta sprint e gere um resumo executivo t\u00e9cnico, com foco em **sa\u00fade do fluxo de trabalho** e **efici\u00eancia da entrega**.\n\nUse as seguintes refer\u00eancias como base de compara\u00e7\u00e3o (em dias \u00fateis ou propor\u00e7\u00f5es):\n- Cycle time (p90) ideal: \u2264 7 dias\n- Lead time (p90) ideal: \u2264 10 dias\n- Aging: deve ser menor ou igual ao p90 do cycle time\n- Rework: ideal \u2264 5%\n- Bugs: ideal \u2264 10%\n- WIP: ideal \u2264 1,5x o throughput m\u00e9dio da sprint\n\n**N\u00e3o enfatize scope creep ou compromisso vs entrega** \u2014 o fluxo \u00e9 puxado e prioriza estabilidade, n\u00e3o cumprimento de escopo planejado.\n\n**Instru\u00e7\u00f5es:**\n- Seja direto e t\u00e9cnico, sem floreios\n- Use valores do JSON sempre que poss\u00edvel\n- Considere apenas dias \u00fateis (ignore fins de semana) nos c\u00e1lculos de tempo\n- SEMPRE que mencionar uma issue (DH-XXX), inclua o summary: \"DH-XXX: T\u00edtulo\"\n- **AGING/ESPERA**: S\u00f3 considere problema se o item estiver em trabalho ativo ('Em andamento', 'Code Review', 'Valida\u00e7\u00e3o', 'Aceita\u00e7\u00e3o', 'Pronto para Deploy'). IGNORE aging/espera de itens em prepara\u00e7\u00e3o ('Tarefas Pendentes', 'Priorizado', 'Refinamento T\u00e9cnico', 'Design Review', etc.) - \u00e9 NORMAL que fiquem esperando.\n- **DISTRIBUI\u00c7\u00c3O**: O time tem especializa\u00e7\u00e3o t\u00e9cnica. N\u00c3O mencione \"capacidade ociosa\" ou sugira \"puxar novo item\" para pessoas com poucos itens. Distribui\u00e7\u00e3o desigual \u00e9 NORMAL devido a skills diferentes.\n- **IMPORTANTE: Inclua TODAS as pessoas em per_participant** \u2014 use metrics.distributions.by_assignee. N\u00e3o omita ningu\u00e9m.\n- Retorne SOMENTE JSON v\u00e1lido\n- Copie sprint_name e sprint_id do input\n\n**Formato de sa\u00edda:**\n{\n  \"sprint_name\": \"{{ $json.sprint.name }}\",\n  \"sprint_id\": \"{{ $json.sprint.id }}\",\n  \"executive_summary_html\": \"1-2 par\u00e1grafos em HTML com panorama geral da fluidez, gargalos e estabilidade do sistema\",\n  \"metrics_summary\": [\n    {\n      \"metric\": \"Cycle Time (p90)\",\n      \"value\": \"X dias\",\n      \"status\": \"ideal | aceit\u00e1vel | alerta\",\n      \"comment\": \"breve explica\u00e7\u00e3o\"\n    }\n  ],\n  \"key_insights\": [\n    {\n      \"title\": \"t\u00edtulo curto\",\n      \"description\": \"observa\u00e7\u00e3o pr\u00e1tica sobre aging/espera de itens em TRABALHO ATIVO, balanceamento e foco. N\u00c3O mencione espera/aging de itens em prepara\u00e7\u00e3o.\",\n      \"action\": \"a\u00e7\u00e3o recomendada\",\n      \"priority\": \"alta | m\u00e9dia | baixa\"\n    }\n  ],\n  \"wip_health\": {\n    \"current\": \"n\u00famero\",\n    \"aging_hotspots\": [\n      {\"key\":\"DH-XXX\",\"summary\":\"t\u00edtulo\",\"assignee\":\"...\",\"status\":\"...\",\"aging_days\":0}\n    ]\n  },\n  \"per_participant\": [\n    {\"person\":\"nome (TODAS as pessoas de metrics.distributions.by_assignee)\",\"observation\":\"breve observa\u00e7\u00e3o sobre balanceamento de carga. N\u00c3O mencione capacidade ociosa - o time tem especializa\u00e7\u00e3o.\",\"suggestion\":\"sugest\u00e3o focada em qualidade/efici\u00eancia, n\u00e3o em volume\"},\n    {\"person\":\"Unassigned\",\"observation\":\"SOMENTE mencione se houver itens Unassigned nos seguintes status: 'Em andamento', 'Code Review', 'Valida\u00e7\u00e3o', 'Aceita\u00e7\u00e3o', 'Pronto para Deploy'. Ignore completamente itens Unassigned em status de prepara\u00e7\u00e3o/discovery como: 'Tarefas Pendentes', 'Priorizado', 'Refinamento T\u00e9cnico', 'Design Review', 'Ideias e Insights', 'Explora\u00e7\u00e3o e An\u00e1lise', 'Prototipagem e Valida\u00e7\u00e3o', 'To Do' - nesses casos \u00e9 ESPERADO que n\u00e3o tenham assignee.\",\"suggestion\":\"atribuir respons\u00e1vel para itens em trabalho ativo\"}\n  ],\n  \"rationale\": \"1-2 frases explicando o porqu\u00ea das tend\u00eancias\"\n}\n\n**Dados da sprint:**\n{{ JSON.stringify($json) }}"
            }
          ]
        },
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.googleGemini",
      "typeVersion": 1,
      "position": [
        1792,
        -64
      ],
      "id": "0e5f0155-7a1f-4d4d-a40d-3552d0eb8d0b",
      "name": "Message a model",
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Extrai o JSON do Gemini (que vem dentro de markdown code block)\nconst rawGemini = $json;\n\nlet geminiResponse;\ntry {\n  let textContent = rawGemini.content?.parts?.[0]?.text || JSON.stringify(rawGemini);\n  \n  // Remove markdown code blocks\n  textContent = textContent.trim();\n  if (textContent.startsWith('```json')) {\n    textContent = textContent.replace(/^```json\\s*/g, '').replace(/```\\s*$/g, '');\n  } else if (textContent.startsWith('```')) {\n    textContent = textContent.replace(/^```\\s*/g, '').replace(/```\\s*$/g, '');\n  }\n  \n  geminiResponse = JSON.parse(textContent);\n  \n} catch (error) {\n  throw new Error(`Erro ao processar Gemini: ${error.message}`);\n}\n\nconst sprintName = geminiResponse.sprint_name || 'Sprint Review';\nconst sprintId = geminiResponse.sprint_id || '';\n\n// Gera HTML para Confluence (formato Storage)\nfunction generateConfluenceHTML(data) {\n  let html = '<ac:structured-macro ac:name=\"info\">';\n  html += '<ac:rich-text-body>';\n  html += `<p><strong>Sprint:</strong> ${data.sprint_name || sprintName} (ID: ${data.sprint_id || sprintId})</p>`;\n  html += '</ac:rich-text-body>';\n  html += '</ac:structured-macro>';\n  \n  // Resumo Executivo\n  html += '<h2>\ud83d\udcca Resumo Executivo</h2>';\n  html += data.executive_summary_html || '<p>Sem resumo dispon\u00edvel.</p>';\n  \n  // Tabela de M\u00e9tricas (NOVO FORMATO)\n  if (data.metrics_summary && data.metrics_summary.length > 0) {\n    html += '<h2>\ud83d\udcc8 M\u00e9tricas</h2>';\n    html += '<table><tbody>';\n    html += '<tr><th>M\u00e9trica</th><th>Valor</th><th>Status</th><th>Coment\u00e1rio</th></tr>';\n    for (const m of data.metrics_summary) {\n      const statusIcon = m.status === 'ideal' ? '\ud83d\udfe2' : (m.status === 'aceit\u00e1vel' ? '\ud83d\udfe1' : '\ud83d\udd34');\n      html += `<tr>`;\n      html += `<td>${m.metric}</td>`;\n      html += `<td><strong>${m.value}</strong></td>`;\n      html += `<td>${statusIcon} ${m.status}</td>`;\n      html += `<td>${m.comment || ''}</td>`;\n      html += `</tr>`;\n    }\n    html += '</tbody></table>';\n  }\n  \n  // WIP Health\n  if (data.wip_health) {\n    html += '<h2>\ud83d\udd04 Sa\u00fade do WIP</h2>';\n    html += `<p><strong>WIP Atual:</strong> ${data.wip_health.current || 0}</p>`;\n    \n    if (data.wip_health.aging_hotspots && data.wip_health.aging_hotspots.length > 0) {\n      html += '<h3>\u26a0\ufe0f Hotspots de Aging</h3>';\n      html += '<table><tbody>';\n      html += '<tr><th>Issue</th><th>Assignee</th><th>Status</th><th>Aging (dias)</th></tr>';\n      for (const item of data.wip_health.aging_hotspots) {\n        const issueDisplay = item.summary ? `${item.key}: ${item.summary}` : item.key;\n        html += `<tr><td>${issueDisplay}</td><td>${item.assignee}</td><td>${item.status}</td><td>${item.aging_days}</td></tr>`;\n      }\n      html += '</tbody></table>';\n    }\n  }\n  \n  // Key Insights (NOVO FORMATO)\n  if (data.key_insights && data.key_insights.length > 0) {\n    html += '<h2>\ud83d\udca1 Insights Acion\u00e1veis</h2>';\n    for (const insight of data.key_insights) {\n      const priorityIcon = insight.priority === 'alta' ? '\ud83d\udd34' : (insight.priority === 'm\u00e9dia' ? '\ud83d\udfe1' : '\ud83d\udfe2');\n      const title = insight.title || insight.theme || 'Insight';\n      html += '<ac:structured-macro ac:name=\"expand\">';\n      html += `<ac:parameter ac:name=\"title\">${priorityIcon} ${title}</ac:parameter>`;\n      html += '<ac:rich-text-body>';\n      html += `<p><strong>Descri\u00e7\u00e3o:</strong> ${insight.description || insight.evidence || 'N/A'}</p>`;\n      html += `<p><strong>A\u00e7\u00e3o recomendada:</strong> ${insight.action || 'N/A'}</p>`;\n      html += '</ac:rich-text-body>';\n      html += '</ac:structured-macro>';\n    }\n  }\n  \n  // Por Participante (NOVO FORMATO)\n  if (data.per_participant && data.per_participant.length > 0) {\n    html += '<h2>\ud83d\udc65 Por Participante</h2>';\n    html += '<table><tbody>';\n    html += '<tr><th>Pessoa</th><th>Observa\u00e7\u00e3o</th><th>Sugest\u00e3o</th></tr>';\n    for (const p of data.per_participant) {\n      html += `<tr><td>${p.person}</td><td>${p.observation || p.tone || ''}</td><td>${p.suggestion || p.suggested_next_step || ''}</td></tr>`;\n    }\n    html += '</tbody></table>';\n  }\n  \n  // Rationale\n  if (data.rationale) {\n    html += '<h2>\ud83c\udfaf An\u00e1lise Geral</h2>';\n    html += `<p>${data.rationale}</p>`;\n  }\n  \n  return html;\n}\n\n// Gera o t\u00edtulo da p\u00e1gina usando o nome REAL da sprint\nconst pageTitle = `Sprint Insights: ${sprintName}`;\n\n// Gera o HTML\nconst htmlBody = generateConfluenceHTML(geminiResponse);\n\nreturn [{\n  json: {\n    pageTitle: pageTitle,\n    spaceKey: 'DHub',\n    htmlBody: htmlBody,\n    geminiData: geminiResponse\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2000,
        -64
      ],
      "id": "f1a2b3c4-d5e6-7890-abcd-ef1234567891",
      "name": "9. Formatar para Confluence"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://gupy-io.atlassian.net/wiki/rest/api/content",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ {\n  \"type\": \"page\",\n  \"title\": $json.pageTitle,\n  \"space\": {\n    \"key\": $json.spaceKey\n  },\n  \"ancestors\": [\n    {\n      \"id\": \"4709056599\"\n    }\n  ],\n  \"body\": {\n    \"storage\": {\n      \"value\": $json.htmlBody,\n      \"representation\": \"storage\"\n    }\n  }\n} }}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2208,
        -64
      ],
      "id": "a9b8c7d6-e5f4-3210-abcd-ef9876543210",
      "name": "10. Publicar no Confluence",
      "credentials": {
        "httpBasicAuth": {
          "name": "<your credential>"
        }
      }
    }
  ],
  "connections": {
    "When clicking 'Execute workflow'": {
      "main": [
        [
          {
            "node": "1. Buscar Sprints Fechadas",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "1. Buscar Sprints Fechadas": {
      "main": [
        [
          {
            "node": "2. Selecionar Sprint",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "2. Selecionar Sprint": {
      "main": [
        [
          {
            "node": "3. Buscar Issues da Sprint (Jira)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "3. Buscar Issues da Sprint (Jira)": {
      "main": [
        [
          {
            "node": "4. Preparar Lista de Issues",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "4. Preparar Lista de Issues": {
      "main": [
        [
          {
            "node": "5. Buscar Detalhes Completos",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "5. Buscar Detalhes Completos": {
      "main": [
        [
          {
            "node": "6. Consolidar Issues",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "6. Consolidar Issues": {
      "main": [
        [
          {
            "node": "7. Calcular M\u00e9tricas",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "7. Calcular M\u00e9tricas": {
      "main": [
        [
          {
            "node": "Message a model",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Message a model": {
      "main": [
        [
          {
            "node": "9. Formatar para Confluence",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "9. Formatar para Confluence": {
      "main": [
        [
          {
            "node": "10. Publicar no Confluence",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "bddf7e29-1138-41cd-9090-dd253ae6e31d",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "id": "Iyfe8eBToZCNxkft",
  "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

JIRA copy. Uses httpRequest, jira, googleGemini. Event-driven trigger; 11 nodes.

Source: https://github.com/amandanery-o/metricas_fluxo/blob/618bf0f629e5e9bcac8f285af2c8a65a74bf5c1e/workflows/sprint.json — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

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

AI & RAG

This workflow helps you repurpose your YouTube videos across multiple social media platforms with zero manual effort. It’s designed for creators, businesses, and marketers who want to maximize reach w

HTTP Request, RSS Feed Read, Discord +4
AI & RAG

Sales Lead Qualifier. Uses telegramTrigger, googleSheets, telegram, googleGemini. Event-driven trigger; 41 nodes.

Telegram Trigger, Google Sheets, Telegram +3
AI & RAG

This workflow empowers marketing teams, agencies and solopreneurs to instantly generate on-brand, platform-optimized social media ads — without designers or complex setup. Running performance marketin

Form Trigger, HTTP Request, Slack +1
AI & RAG

Monitor a Google Drive folder, process each image based on the prompt defined in and save the new image to the specified output Google Drive folder. Maintain a processing log in Google Sheets.

Google Drive Trigger, Google Drive, HTTP Request +2
AI & RAG

[](https://drive.google.com/file/d/1Cl0KwgRgcuBPVdGgL-nqAcheyvfVXttD/view) Click on the image to see the Example output in google drive

HTTP Request, Stop And Error, OpenAI +3