id: ai-github-issue-triage-routing
namespace: io.kestra.blueprints.ai
inputs:
- id: repo_owner
type: STRING
required: true
defaults: "kestra-io"
description: "GitHub repository owner (organization or user)."
- id: repo_name
type: STRING
required: true
defaults: "kestra"
description: "GitHub repository name."
- id: issue_number
type: INT
required: true
defaults: 1001
description: "GitHub issue number to triage."
- id: issue_title
type: STRING
required: true
defaults: "App crashes when clicking submit button on settings page"
description: "Issue title for triage."
- id: issue_body
type: STRING
required: true
defaults: |
Steps to reproduce:
1. Navigate to Settings page
2. Modify notifications setting
3. Click Save Changes button
Expected: Settings update confirmation toast appears
Actual: Uncaught TypeError: Cannot read properties of undefined (reading 'submit')
Browser: Chrome 132
description: "Issue body/description for AI analysis."
- id: issue_url
type: STRING
required: true
defaults: "https://github.com/kestra-io/kestra/issues/1001"
description: "Direct URL to the GitHub issue."
- id: issue_author
type: STRING
required: true
defaults: "community-contributor"
description: "GitHub username of the issue author."
- id: dry_run
type: BOOL
required: true
defaults: false
description: "When true, runs entirely locally with zero external API calls (no
GitHub, OpenAI, or Slack requests)."
variables:
# Execution defaults and state tracking
is_dry_run: "false"
repo_owner: "kestra-io"
repo_name: "kestra"
issue_number: 1001
issue_title: "Untitled Issue"
issue_body: "No description provided"
issue_url: "https://github.com/kestra-io/kestra/issues/1001"
issue_author: "unknown"
ai_category: "other"
ai_priority: "medium"
ai_summary: "Automated triage classification"
ai_reasoning: "Awaiting triage classification"
target_label: "needs-triage"
# GitHub API configuration (without trailing slash)
github_api_base: "https://api.github.com"
github_repo_owner: "kestra-io"
github_repo_name: "kestra"
# AI Model parameters
ai_model: "gpt-4o-mini"
ai_temperature: 0.2
# Label mappings (Category -> GitHub Label name)
label_bug: "bug"
label_feature: "enhancement"
label_documentation: "documentation"
label_question: "question"
label_other: "needs-triage"
# Supported AI classification categories
classification_categories:
- bug
- feature
- documentation
- question
- other
triggers:
- id: github_webhook
type: io.kestra.plugin.core.trigger.Webhook
description: "Receives GitHub webhook events (e.g. issues opened or labeled)."
key: "github-webhook"
tasks:
- id: extract_issue_data
type: io.kestra.plugin.core.execution.SetVariables
description: "Extract and normalize issue data from webhook event payload or
manual input parameters."
variables:
repo_owner: "{{ trigger.body.repository.owner.login ?? (inputs.repo_owner ??
vars.github_repo_owner) }}"
repo_name: "{{ trigger.body.repository.name ?? (inputs.repo_name ??
vars.github_repo_name) }}"
issue_number: "{{ trigger.body.issue.number ?? (inputs.issue_number ?? 1001) }}"
issue_title: "{{ trigger.body.issue.title ?? (inputs.issue_title ?? 'Untitled
Issue') }}"
issue_body: "{{ trigger.body.issue.body ?? (inputs.issue_body ?? 'No description
provided') }}"
issue_url: "{{ trigger.body.issue.html_url ?? (inputs.issue_url ??
('https://github.com/' ~ (trigger.body.repository.owner.login ??
(inputs.repo_owner ?? vars.github_repo_owner)) ~ '/' ~
(trigger.body.repository.name ?? (inputs.repo_name ??
vars.github_repo_name)) ~ '/issues/' ~ (trigger.body.issue.number ??
(inputs.issue_number ?? 1001)))) }}"
issue_author: "{{ trigger.body.issue.user.login ?? (inputs.issue_author ??
'unknown') }}"
is_dry_run: >-
{%- if trigger.parameters.dry_run is defined and
trigger.parameters.dry_run is not empty -%}
{{ (trigger.parameters.dry_run | first) == 'true' ? 'true' : 'false' }}
{%- else -%}
{{ (inputs.dry_run ?? false) == true ? 'true' : 'false' }}
{%- endif -%}
- id: fetch_issue_metadata
type: io.kestra.plugin.core.http.Request
description: "Fetch live issue details from GitHub REST API to retrieve current
issue state and metadata."
runIf: "{{ vars.is_dry_run != 'true' }}"
uri: "{{ vars.github_api_base }}/repos/{{ vars.repo_owner }}/{{ vars.repo_name
}}/issues/{{ vars.issue_number }}"
method: GET
headers:
Accept: application/vnd.github+json
Authorization: "Bearer {{ secret('GITHUB_TOKEN') }}"
X-GitHub-Api-Version: "2022-11-28"
retry:
type: constant
interval: PT2S
maxAttempts: 3
- id: classify_issue
type: io.kestra.plugin.openai.ChatCompletion
description: "Analyze issue contents using OpenAI LLM with strict JSON schema
output (skipped when dry_run=true)."
runIf: "{{ vars.is_dry_run != 'true' }}"
apiKey: "{{ secret('OPENAI_API_KEY') }}"
model: "{{ vars.ai_model }}"
temperature: 0.2
messages:
- role: system
content: |
You are an expert GitHub issue triage bot for open source repositories.
Analyze the provided issue title and description.
Classify the issue into exactly one of the allowed categories: {{ vars.classification_categories | toJson }}.
Assign an appropriate priority: "critical", "high", "medium", or "low".
Provide a concise short_summary (max 100 characters) and a brief reasoning (max 180 characters).
You MUST respond strictly with a valid JSON object matching the schema.
- role: user
content: |
Repository: {{ vars.repo_owner }}/{{ vars.repo_name }}
Issue #{{ vars.issue_number }}: {{ vars.issue_title }}
Author: @{{ vars.issue_author }}
Description:
{{ vars.issue_body }}
jsonResponseSchema: |
{
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": ["bug", "feature", "documentation", "question", "other"]
},
"short_summary": {
"type": "string"
},
"priority": {
"type": "string",
"enum": ["critical", "high", "medium", "low"]
},
"reasoning": {
"type": "string"
}
},
"required": ["category", "short_summary", "priority", "reasoning"],
"additionalProperties": false
}
retry:
type: constant
interval: PT3S
maxAttempts: 3
- id: mock_classification
type: io.kestra.plugin.core.debug.Return
description: "Deterministic mock classification output for dry-run mode without
external API calls."
runIf: "{{ vars.is_dry_run == 'true' }}"
format: |
{
"category": "bug",
"short_summary": "Simulated bug triage: form submission error on settings page",
"priority": "high",
"reasoning": "Dry-run deterministic mock classification"
}
- id: parse_and_validate_ai_response
type: io.kestra.plugin.core.execution.SetVariables
description: "Safely parse AI JSON response, validate triage fields with
fallbacks, and compute target label."
variables:
ai_category: >-
{%- set is_dry = vars.is_dry_run == 'true' -%} {%- set raw = is_dry ?
outputs.mock_classification.value :
(outputs.classify_issue.choices[0].message.content ?? '{}') -%} {%- set
parsed = (raw is json) ? ((raw | jq('.category') | first) | trim) :
'other' -%} {%- if parsed == 'bug' or parsed == 'feature' or parsed ==
'documentation' or parsed == 'question' -%}{{ parsed }}{%- else
-%}other{%- endif -%}
ai_priority: >-
{%- set is_dry = vars.is_dry_run == 'true' -%} {%- set raw = is_dry ?
outputs.mock_classification.value :
(outputs.classify_issue.choices[0].message.content ?? '{}') -%} {%- set
parsed = (raw is json) ? ((raw | jq('.priority') | first) | trim) :
'medium' -%} {%- if parsed == 'critical' or parsed == 'high' or parsed
== 'medium' or parsed == 'low' -%}{{ parsed }}{%- else -%}medium{%-
endif -%}
ai_summary: >-
{%- set is_dry = vars.is_dry_run == 'true' -%} {%- set raw = is_dry ?
outputs.mock_classification.value :
(outputs.classify_issue.choices[0].message.content ?? '{}') -%} {%- set
parsed = (raw is json) ? ((raw | jq('.short_summary') | first) | trim) :
null -%} {{ parsed ?? vars.issue_title }}
ai_reasoning: >-
{%- set is_dry = vars.is_dry_run == 'true' -%} {%- set raw = is_dry ?
outputs.mock_classification.value :
(outputs.classify_issue.choices[0].message.content ?? '{}') -%} {%- set
parsed = (raw is json) ? ((raw | jq('.reasoning') | first) | trim) :
null -%} {{ parsed ?? 'Automated triage classification' }}
target_label: >-
{%- set is_dry = vars.is_dry_run == 'true' -%} {%- set raw = is_dry ?
outputs.mock_classification.value :
(outputs.classify_issue.choices[0].message.content ?? '{}') -%} {%- set
cat = (raw is json) ? ((raw | jq('.category') | first) | trim) : 'other'
-%} {%- if cat == 'bug' -%}{{ vars.label_bug }} {%- elseif cat ==
'feature' -%}{{ vars.label_feature }} {%- elseif cat == 'documentation'
-%}{{ vars.label_documentation }} {%- elseif cat == 'question' -%}{{
vars.label_question }} {%- else -%}{{ vars.label_other }}{%- endif -%}
- id: tag_execution
type: io.kestra.plugin.core.execution.Labels
description: "Attach triage metadata to current Kestra execution for easy
searching and filtering."
labels:
repository: "{{ vars.repo_owner }}_{{ vars.repo_name }}"
issue: "{{ vars.issue_number }}"
category: "{{ vars.ai_category }}"
priority: "{{ vars.ai_priority }}"
- id: branch_on_classification
type: io.kestra.plugin.core.flow.If
description: "Branch workflow execution: apply bug label on bug classification
vs appropriate label on others."
condition: "{{ (vars.ai_category | trim) == 'bug' }}"
then:
- id: apply_bug_label
type: io.kestra.plugin.core.http.Request
description: "Apply 'bug' label to GitHub issue via GitHub REST API."
runIf: "{{ vars.is_dry_run != 'true' }}"
uri: "{{ vars.github_api_base }}/repos/{{ vars.repo_owner }}/{{ vars.repo_name
}}/issues/{{ vars.issue_number }}/labels"
method: POST
contentType: application/json
headers:
Accept: application/vnd.github+json
Authorization: "Bearer {{ secret('GITHUB_TOKEN') }}"
X-GitHub-Api-Version: "2022-11-28"
body: |
{
"labels": ["{{ vars.label_bug }}"]
}
retry:
type: constant
interval: PT2S
maxAttempts: 3
- id: log_bug_triage
type: io.kestra.plugin.core.log.Log
description: "Log triage verdict for bug flow."
message: "Bug triage executed for #{{ vars.issue_number }} (Priority: {{
vars.ai_priority }}). Label '{{ vars.label_bug }}' applied."
else:
- id: apply_categorized_label
type: io.kestra.plugin.core.http.Request
description: "Apply categorized label (enhancement, documentation, question,
needs-triage) via GitHub REST API."
runIf: "{{ vars.is_dry_run != 'true' }}"
uri: "{{ vars.github_api_base }}/repos/{{ vars.repo_owner }}/{{ vars.repo_name
}}/issues/{{ vars.issue_number }}/labels"
method: POST
contentType: application/json
headers:
Accept: application/vnd.github+json
Authorization: "Bearer {{ secret('GITHUB_TOKEN') }}"
X-GitHub-Api-Version: "2022-11-28"
body: |
{
"labels": ["{{ vars.target_label }}"]
}
retry:
type: constant
interval: PT2S
maxAttempts: 3
- id: log_other_triage
type: io.kestra.plugin.core.log.Log
description: "Log triage verdict for non-bug flow."
message: "Standard triage executed for #{{ vars.issue_number }}: classified as
'{{ vars.ai_category }}' (Priority: {{ vars.ai_priority }}). Label '{{
vars.target_label }}' applied."
- id: send_slack_notification
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
description: "Post structured triage summary to team Slack channel via incoming
webhook."
runIf: "{{ vars.is_dry_run != 'true' }}"
url: "{{ secret('SLACK_WEBHOOK_URL') }}"
payload: |
{
"text": "GitHub Issue #{{ vars.issue_number }} triaged as [{{ vars.ai_category | upper }}] (Priority: {{ vars.ai_priority | upper }})",
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "🤖 GitHub Issue Triaged & Routed",
"emoji": true
}
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": "*Repository:*\n<https://github.com/{{ vars.repo_owner }}/{{ vars.repo_name }}|{{ vars.repo_owner }}/{{ vars.repo_name }}>"
},
{
"type": "mrkdwn",
"text": "*Issue:*\n<{{ vars.issue_url }}|#{{ vars.issue_number }} - {{ vars.issue_title }}>"
},
{
"type": "mrkdwn",
"text": "*Classification:*\n`{{ vars.ai_category }}`"
},
{
"type": "mrkdwn",
"text": "*Priority:*\n`{{ vars.ai_priority | upper }}`"
},
{
"type": "mrkdwn",
"text": "*Applied Label:*\n`{{ vars.target_label }}`"
},
{
"type": "mrkdwn",
"text": "*Author:*\n@{{ vars.issue_author }}"
}
]
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Summary:*\n{{ vars.ai_summary }}\n\n*Reasoning:*\n_{{ vars.ai_reasoning }}_"
}
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "View Issue on GitHub",
"emoji": true
},
"url": "{{ vars.issue_url }}",
"style": "{{ vars.ai_category == 'bug' ? 'danger' : 'primary' }}"
}
]
}
]
}
errors:
- id: on_failure_alert
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
description: "Alert Slack channel if triage workflow encounters an unhandled failure."
runIf: "{{ vars.is_dry_run != 'true' }}"
url: "{{ secret('SLACK_WEBHOOK_URL') }}"
payload: |
{
"text": "⚠️ *GitHub Issue Triage Failed* for repository `{{ vars.repo_owner ?? 'unknown' }}/{{ vars.repo_name ?? 'unknown' }}` on issue `#{{ vars.issue_number ?? 'unknown' }}`. Please inspect Kestra execution `{{ execution.id }}`."
}
- id: log_failure
type: io.kestra.plugin.core.log.Log
description: "Log failure details for execution debugging."
level: ERROR
message: "Issue triage execution {{ execution.id }} failed on repository {{
vars.repo_owner }}/{{ vars.repo_name }} for issue #{{ vars.issue_number
}}."