Tutorial

How to Build a Slack Bot That Routes Jira Tickets with AI

April 2026 · 8 min read

Most Slack-to-Jira integrations stop at "create a ticket." You type /jira create, fill a form, the ticket lands in a backlog — and someone still has to manually route it to the right team. The value is in the routing, not the creation. This post covers the full stack: Slack modal → AI classification → Jira ticket creation → correct queue assignment → Slack confirmation.

This is running in production across three workspaces. All code is Python + Flask + Slack Bolt.

Prerequisites

  • A Slack app with chat:write, commands, and views:open scopes
  • A Jira Service Management project (or classic Jira with issue types)
  • Any LLM API — we use OpenAI GPT-4o-mini for classification ($0.003 per 1K tokens)
  • Python 3.11+, slack-bolt, jira, openai packages

Step 1: The Slack Modal

Don't use a slash command that accepts free text. Use a modal — it forces structure and gives you the summary + description fields you need for accurate classification.

@app.command("/support")
def open_support_modal(ack, body, client):
 ack()
 client.views_open(
 trigger_id=body["trigger_id"],
 view={
 "type": "modal",
 "callback_id": "support_ticket",
 "title": {"type": "plain_text", "text": "IT Support"},
 "submit": {"type": "plain_text", "text": "Submit"},
 "blocks": [
 {
 "type": "input",
 "block_id": "summary",
 "element": {
 "type": "plain_text_input",
 "action_id": "summary_input",
 "placeholder": {"type": "plain_text", "text": "One sentence: what do you need?"}
 },
 "label": {"type": "plain_text", "text": "Summary"}
 },
 {
 "type": "input",
 "block_id": "description",
 "optional": True,
 "element": {
 "type": "plain_text_input",
 "action_id": "description_input",
 "multiline": True,
 "placeholder": {"type": "plain_text", "text": "More context (optional)"}
 },
 "label": {"type": "plain_text", "text": "Details"}
 }
 ]
 }
 )

The callback_id is what ties the submission to your handler. Keep the form short — two fields is enough for classification. Every extra required field reduces submission rate.

Step 2: AI Classification

The classifier runs on modal submission, before the Jira ticket is created. You have about 3 seconds before Slack shows an error, so keep the LLM call fast — use a small model.

CATEGORIES = {
 "IT_HARDWARE": {
 "jira_project": "IT",
 "issue_type": "Hardware Issue",
 "assignee_group": "it-hardware",
 "priority": "Medium"
 },
 "SOFTWARE_ACCESS": {
 "jira_project": "IT",
 "issue_type": "Access Request",
 "assignee_group": "it-software",
 "priority": "Low"
 },
 "DEV_TOOLS": {
 "jira_project": "DEVOPS",
 "issue_type": "Task",
 "assignee_group": "engineering-ops",
 "priority": "Medium"
 },
 "HR_SYSTEMS": {
 "jira_project": "HR",
 "issue_type": "Service Request",
 "assignee_group": "hr-team",
 "priority": "Low"
 }
}

def classify(summary: str, description: str) -> str:
 response = openai_client.chat.completions.create(
 model="gpt-4o-mini",
 messages=[
 {
 "role": "system",
 "content": (
 "Classify IT support tickets. "
 "Reply with exactly one of: IT_HARDWARE, SOFTWARE_ACCESS, DEV_TOOLS, HR_SYSTEMS, UNKNOWN. "
 "No explanation."
 )
 },
 {
 "role": "user",
 "content": f"Summary: {summary}\nDetails: {description or 'none'}"
 }
 ],
 max_tokens=10,
 temperature=0
 )
 result = response.choices[0].message.content.strip()
 return result if result in CATEGORIES else "UNKNOWN"

Two things worth noting in this implementation:

  1. System prompt is a constraint, not a description. "Reply with exactly one of: X, Y, Z" produces cleaner output than "You are an expert classifier that."
  2. max_tokens=10 caps cost and latency. Category names are short — there's no reason to let the model ramble.

Step 3: Create the Jira Ticket

def create_jira_ticket(summary: str, description: str, category: str, reporter_email: str):
 config = CATEGORIES.get(category, CATEGORIES["IT_HARDWARE"])

 issue = jira_client.create_issue(
 project=config["jira_project"],
 summary=summary,
 description=description or "No additional details provided.",
 issuetype={"name": config["issue_type"]},
 priority={"name": config["priority"]},
 labels=[f"slack-bot", f"category:{category.lower()}"],
 customfield_10050=reporter_email # reporter field (your field ID will differ)
 )

 # Assign to group via Jira automation rule (preferred) or directly
 # Direct assignment requires knowing a specific user — use Jira automation
 # rule "When: issue created AND label = category:it_hardware → Assign to group"

 return issue.key, issue.permalink()

A note on assignment: don't assign to a specific user in code. Teams change. Use a Jira automation rule triggered by the label — this keeps the routing logic in Jira where non-engineers can update it, not buried in Python.

Step 4: Wire It Together and Confirm in Slack

@app.view("support_ticket")
def handle_submission(ack, body, view, client):
 ack()

 summary = view["state"]["values"]["summary"]["summary_input"]["value"]
 description = view["state"]["values"]["description"]["description_input"].get("value", "")
 user_id = body["user"]["id"]

 # Get user email for Jira reporter field
 user_info = client.users_info(user=user_id)
 email = user_info["user"]["profile"].get("email", "unknown@company.com")

 # Classify
 category = classify(summary, description)

 # Create ticket
 ticket_key, ticket_url = create_jira_ticket(summary, description, category, email)

 # Confirm in Slack (DM the submitter)
 client.chat_postMessage(
 channel=user_id,
 text=f":white_check_mark: Ticket created: *{ticket_key}*\n"
 f"*Category:* {category.replace('_', ' ').title()}\n"
 f"*Summary:* {summary}\n"
 f"<{ticket_url}|View in Jira>"
 )

The DM confirmation closes the loop that kills informal Slack follow-up. Users know their ticket landed, know what it was classified as, and have a link. If the classification looks wrong, they can comment on the Jira ticket or re-submit with a correction note.

Handling UNKNOWN Classifications

Don't silently create an unrouted ticket. When the classifier returns UNKNOWN, route to a triage channel and notify an admin:

if category == "UNKNOWN":
 ticket_key, ticket_url = create_jira_ticket(
 summary, description, "IT_HARDWARE", email # safe default project
 )
 # Alert the triage channel
 client.chat_postMessage(
 channel="#it-triage",
 text=(
 f":question: Unclassified ticket from <@{user_id}>: *{ticket_key}*\n"
 f"*Summary:* {summary}\n"
 f"Manual routing needed. <{ticket_url}|View in Jira>"
 )
 )

In production, UNKNOWN hits about 4% of tickets. The triage channel means nothing falls through — it just needs a human for those 4 cases per 100.

Deployment

This runs on a DigitalOcean App Platform instance (512MB RAM, €5/month). The full app is about 200 lines of Python. Flask handles the Slack event URL; Slack Bolt handles the routing.

One gotcha: Slack requires your event URL to respond within 3 seconds. If your LLM call + Jira API call exceed that, you'll get timeout errors. The fix is to acknowledge immediately and process in a background thread:

@app.view("support_ticket")
def handle_submission(ack, body, view, client):
 ack() # acknowledge immediately — Slack's 3s clock stops here
 # process in background
 threading.Thread(
 target=_process_ticket,
 args=(body, view, client)
 ).start()
~200
Lines of Python total
96%
Classification accuracy
€5/mo
Hosting cost

What to Build Next

Once this is running, two extensions add significant value:

  • SLA breach alerts. Query Jira every 15 minutes for tickets approaching their SLA deadline and post to the assignee's Slack DM. See the SLA automation post for the full implementation.
  • Status updates back to Slack. Use a Jira webhook to DM the original requester when their ticket status changes. Closes the loop entirely — no more "what's happening with my request?" messages.

Get the Full Source Code

The complete Slack-to-Jira bot (~200 lines): Flask app, classifier, Jira integration, and the DigitalOcean deploy config.

Related Service

Custom Slack Bots for Engineering Teams

I build production Slack bots with AI classification, Jira integration, and multi-workspace support. Fixed price, full code ownership, deployed in under 3 weeks.

Learn more →

Related Posts

Jira SLA Automation: 65% Faster Response Time

The SLA breach alert system that pairs with this bot.

Multi-Tenant Slack Bot: One App, Three Workspaces

Scale this architecture to multiple Slack workspaces.

Evgeny Goncharov - Founder of TechConcepts, ex-Big 4 Advisory

Evgeny Goncharov

Founder, TechConcepts

I build automation tools and custom software for businesses. Previously at a major search platform and Big 4 Advisory. Based in Madrid.

About me LinkedIn GitHub
← All blog posts

Want a Slack bot that actually routes tickets?

15 minutes. I'll tell you if I can build it for your stack and give you a rough scope.

Book a Free Call