All articles

Automating Github Issues Responses with Invero

Learn how to automatically respond to Github Issues with AI.

Karl Jones

Every open source project or developer tool eventually hits the same wall: your GitHub Issues queue becomes a support forum, and your team spends hours answering questions that your documentation already covers. Maintainers burn out. Response times slip. Users feel ignored.

What if your repo could answer common questions automatically - in seconds, from your own docs - the moment a new issue lands?

In this tutorial, you'll build a GitHub Action that listens for new issues, queries the Invero API with the issue content, and posts an AI-generated answer as a comment. The whole setup takes under 20 minutes, requires no extra infrastructure, and works entirely within GitHub's native tooling.

What We're Building

Here's the flow end-to-end:

  1. A user opens a new GitHub Issue

  2. A GitHub Actions workflow triggers on the `issues.opened` event

  3. The workflow sends the issue title and body to the Invero API

  4. Invero searches your ingested documentation and generates a relevant answer

  5. The workflow posts that answer as a comment on the issue, with a note that it was AI-generated

The result: users get an instant, docs-grounded first response while your team reviews the issue in parallel.

Requirements

Before you start, make sure you have:

  • A Invero account with a project set up and your documentation already ingested.

  • Your Invero API credentials - specifically a Project ID, Integration ID and an API key.

  • A GitHub repository where you have admin access (to add secrets and create workflow files).

  • Basic familiarity with GitHub Actions YAML syntax.

Step 1: Get your Invero credentials

Log into your Invero dashboard and navigate to your project settings. You'll need two values:

  • Project ID - the unique identifier for your Invero project, this can be found as the first ID within the URL for the dashboard

  • Integration ID - the unique identifier for your Invero integration, create a new "Custom (via API)" integration within the integrations section of the Invero dashboard

  • API Key - your secret key for authenticating requests to the Invero API, navigate to "API Keys" within the Invero dashboard

Keep these handy - you'll be adding them as GitHub secrets in the next step.

Step 2: Add secrets to your Github Repository

Never hard-code credentials in workflow files. GitHub Secrets are the right place for sensitive values.

  1. Go to your GitHub repository

  2. Click Settings > Secrets and variables > Actions

  3. Click New repository secret and add the following three secrets:

    1. INVERO_PROJECT_ID - your Project ID

    2. INVERO_INTEGRATION_ID - your Integration ID

    3. INVERO_API_KEY - your API key

We'll reference these later within the GitHub Action.

Step 3: Create the GitHub Actions Workflow

In your repository, create the following directory structure if it doesn't already exist:





Then, add the workflow file below, we'll walk through each section after the full example.

# .github/workflows/invero-issue-bot.yml
name: Invero Issue Bot

on:
  issues:
    types: [opened]

jobs:
  answer-issue:
    runs-on: ubuntu-latest
    permissions:
      issues: write  # needed to post comments

    steps:
      - name: Answer issue with Invero
        env:
          INVERO_API_KEY: ${{ secrets.INVERO_API_KEY }}
          INVERO_PROJECT_ID: ${{ secrets.INVERO_PROJECT_ID }}
          INVERO_INTEGRATION_ID: ${{ secrets.INVERO_INTEGRATION_ID }}
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          ISSUE_NUMBER: ${{ github.event.issue.number }}
          ISSUE_TITLE: ${{ github.event.issue.title }}
          ISSUE_LABELS: ${{ join(github.event.issue.labels.*.name, ',') }}
          ISSUE_BODY: ${{ github.event.issue.body }}
          REPO: ${{ github.repository }}
        run: |
          pip install requests --quiet
          python3 << 'EOF'
          import os
          import requests
          import json
          import sys

          invero_api_key  = os.environ["INVERO_API_KEY"]
          invero_project  = os.environ["INVERO_PROJECT_ID"]
          invero_integration_id = os.environ["INVERO_INTEGRATION_ID"]
          gh_token      = os.environ["GH_TOKEN"]
          issue_number  = os.environ["ISSUE_NUMBER"]
          issue_title   = os.environ["ISSUE_TITLE"]
          issue_labels  = [l.strip().lower() for l in os.environ.get("ISSUE_LABELS", "").split(",") if l.strip()]
          issue_body    = os.environ.get("ISSUE_BODY", "")
          repo          = os.environ["REPO"]

          # --- 1. Build the question from the issue ---
          question = f"{issue_title}\n\n{issue_body}".strip()

          # --- 2. Check if it should skip ---
          labels_to_skip = ['bug', 'feature request']
          should_skip = any(label in issue_labels for label in labels_to_skip)
          if should_skip:
            sys.exit(0)

          # --- 3. Call the Invero Chat API ---
          invero_url = f"https://api.invero.ai/query/v1/projects/{invero_project}/chat/"
          invero_headers = {
              "X-API-KEY": invero_api_key,
              "Content-Type": "application/json",
          }
          invero_payload = {
              "query": question,
              "integration_id": invero_integration_id
          }

          invero_resp = requests.post(invero_url, headers=invero_headers, json=invero_payload, timeout=60)
          invero_resp.raise_for_status()
          invero_data = invero_resp.json()

          # Extract the answer text (adjust key if your Invero version differs)
          answer = invero_data.get("answer") or invero_data.get("message") or json.dumps(invero_data)

          # Optionally include relevant source links if Invero returns them
          sources = invero_data.get("relevant_sources", [])
          if sources:
              links = "\n".join(f"- [{s.get('title', s.get('url', ''))}]({s.get('url', '')})" for s in sources[:5])
              answer += f"\n\n**Relevant sources:**\n{links}"

          # --- 3. Post the answer as a GitHub issue comment ---
          comment_body = (
              f"๐Ÿ‘‹ **Invero bot here!** I found a potential answer to your question:\n\n"
              f"{answer}\n\n"
              f"---\n"
              f"*This answer was generated automatically. If it didn't help, a human will follow up.*"
          )

          gh_url = f"https://api.github.com/repos/{repo}/issues/{issue_number}/comments"
          gh_headers = {
              "Authorization": f"Bearer {gh_token}",
              "Accept": "application/vnd.github+json",
              "X-GitHub-Api-Version": "2022-11-28",
          }
          gh_resp = requests.post(gh_url, headers=gh_headers, json={"body": comment_body}, timeout=30)
          gh_resp.raise_for_status()

          print(f"โœ… Posted Invero answer to issue #{issue_number}")
          EOF
# .github/workflows/invero-issue-bot.yml
name: Invero Issue Bot

on:
  issues:
    types: [opened]

jobs:
  answer-issue:
    runs-on: ubuntu-latest
    permissions:
      issues: write  # needed to post comments

    steps:
      - name: Answer issue with Invero
        env:
          INVERO_API_KEY: ${{ secrets.INVERO_API_KEY }}
          INVERO_PROJECT_ID: ${{ secrets.INVERO_PROJECT_ID }}
          INVERO_INTEGRATION_ID: ${{ secrets.INVERO_INTEGRATION_ID }}
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          ISSUE_NUMBER: ${{ github.event.issue.number }}
          ISSUE_TITLE: ${{ github.event.issue.title }}
          ISSUE_LABELS: ${{ join(github.event.issue.labels.*.name, ',') }}
          ISSUE_BODY: ${{ github.event.issue.body }}
          REPO: ${{ github.repository }}
        run: |
          pip install requests --quiet
          python3 << 'EOF'
          import os
          import requests
          import json
          import sys

          invero_api_key  = os.environ["INVERO_API_KEY"]
          invero_project  = os.environ["INVERO_PROJECT_ID"]
          invero_integration_id = os.environ["INVERO_INTEGRATION_ID"]
          gh_token      = os.environ["GH_TOKEN"]
          issue_number  = os.environ["ISSUE_NUMBER"]
          issue_title   = os.environ["ISSUE_TITLE"]
          issue_labels  = [l.strip().lower() for l in os.environ.get("ISSUE_LABELS", "").split(",") if l.strip()]
          issue_body    = os.environ.get("ISSUE_BODY", "")
          repo          = os.environ["REPO"]

          # --- 1. Build the question from the issue ---
          question = f"{issue_title}\n\n{issue_body}".strip()

          # --- 2. Check if it should skip ---
          labels_to_skip = ['bug', 'feature request']
          should_skip = any(label in issue_labels for label in labels_to_skip)
          if should_skip:
            sys.exit(0)

          # --- 3. Call the Invero Chat API ---
          invero_url = f"https://api.invero.ai/query/v1/projects/{invero_project}/chat/"
          invero_headers = {
              "X-API-KEY": invero_api_key,
              "Content-Type": "application/json",
          }
          invero_payload = {
              "query": question,
              "integration_id": invero_integration_id
          }

          invero_resp = requests.post(invero_url, headers=invero_headers, json=invero_payload, timeout=60)
          invero_resp.raise_for_status()
          invero_data = invero_resp.json()

          # Extract the answer text (adjust key if your Invero version differs)
          answer = invero_data.get("answer") or invero_data.get("message") or json.dumps(invero_data)

          # Optionally include relevant source links if Invero returns them
          sources = invero_data.get("relevant_sources", [])
          if sources:
              links = "\n".join(f"- [{s.get('title', s.get('url', ''))}]({s.get('url', '')})" for s in sources[:5])
              answer += f"\n\n**Relevant sources:**\n{links}"

          # --- 3. Post the answer as a GitHub issue comment ---
          comment_body = (
              f"๐Ÿ‘‹ **Invero bot here!** I found a potential answer to your question:\n\n"
              f"{answer}\n\n"
              f"---\n"
              f"*This answer was generated automatically. If it didn't help, a human will follow up.*"
          )

          gh_url = f"https://api.github.com/repos/{repo}/issues/{issue_number}/comments"
          gh_headers = {
              "Authorization": f"Bearer {gh_token}",
              "Accept": "application/vnd.github+json",
              "X-GitHub-Api-Version": "2022-11-28",
          }
          gh_resp = requests.post(gh_url, headers=gh_headers, json={"body": comment_body}, timeout=30)
          gh_resp.raise_for_status()

          print(f"โœ… Posted Invero answer to issue #{issue_number}")
          EOF
# .github/workflows/invero-issue-bot.yml
name: Invero Issue Bot

on:
  issues:
    types: [opened]

jobs:
  answer-issue:
    runs-on: ubuntu-latest
    permissions:
      issues: write  # needed to post comments

    steps:
      - name: Answer issue with Invero
        env:
          INVERO_API_KEY: ${{ secrets.INVERO_API_KEY }}
          INVERO_PROJECT_ID: ${{ secrets.INVERO_PROJECT_ID }}
          INVERO_INTEGRATION_ID: ${{ secrets.INVERO_INTEGRATION_ID }}
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          ISSUE_NUMBER: ${{ github.event.issue.number }}
          ISSUE_TITLE: ${{ github.event.issue.title }}
          ISSUE_LABELS: ${{ join(github.event.issue.labels.*.name, ',') }}
          ISSUE_BODY: ${{ github.event.issue.body }}
          REPO: ${{ github.repository }}
        run: |
          pip install requests --quiet
          python3 << 'EOF'
          import os
          import requests
          import json
          import sys

          invero_api_key  = os.environ["INVERO_API_KEY"]
          invero_project  = os.environ["INVERO_PROJECT_ID"]
          invero_integration_id = os.environ["INVERO_INTEGRATION_ID"]
          gh_token      = os.environ["GH_TOKEN"]
          issue_number  = os.environ["ISSUE_NUMBER"]
          issue_title   = os.environ["ISSUE_TITLE"]
          issue_labels  = [l.strip().lower() for l in os.environ.get("ISSUE_LABELS", "").split(",") if l.strip()]
          issue_body    = os.environ.get("ISSUE_BODY", "")
          repo          = os.environ["REPO"]

          # --- 1. Build the question from the issue ---
          question = f"{issue_title}\n\n{issue_body}".strip()

          # --- 2. Check if it should skip ---
          labels_to_skip = ['bug', 'feature request']
          should_skip = any(label in issue_labels for label in labels_to_skip)
          if should_skip:
            sys.exit(0)

          # --- 3. Call the Invero Chat API ---
          invero_url = f"https://api.invero.ai/query/v1/projects/{invero_project}/chat/"
          invero_headers = {
              "X-API-KEY": invero_api_key,
              "Content-Type": "application/json",
          }
          invero_payload = {
              "query": question,
              "integration_id": invero_integration_id
          }

          invero_resp = requests.post(invero_url, headers=invero_headers, json=invero_payload, timeout=60)
          invero_resp.raise_for_status()
          invero_data = invero_resp.json()

          # Extract the answer text (adjust key if your Invero version differs)
          answer = invero_data.get("answer") or invero_data.get("message") or json.dumps(invero_data)

          # Optionally include relevant source links if Invero returns them
          sources = invero_data.get("relevant_sources", [])
          if sources:
              links = "\n".join(f"- [{s.get('title', s.get('url', ''))}]({s.get('url', '')})" for s in sources[:5])
              answer += f"\n\n**Relevant sources:**\n{links}"

          # --- 3. Post the answer as a GitHub issue comment ---
          comment_body = (
              f"๐Ÿ‘‹ **Invero bot here!** I found a potential answer to your question:\n\n"
              f"{answer}\n\n"
              f"---\n"
              f"*This answer was generated automatically. If it didn't help, a human will follow up.*"
          )

          gh_url = f"https://api.github.com/repos/{repo}/issues/{issue_number}/comments"
          gh_headers = {
              "Authorization": f"Bearer {gh_token}",
              "Accept": "application/vnd.github+json",
              "X-GitHub-Api-Version": "2022-11-28",
          }
          gh_resp = requests.post(gh_url, headers=gh_headers, json={"body": comment_body}, timeout=30)
          gh_resp.raise_for_status()

          print(f"โœ… Posted Invero answer to issue #{issue_number}")
          EOF
# .github/workflows/invero-issue-bot.yml
name: Invero Issue Bot

on:
  issues:
    types: [opened]

jobs:
  answer-issue:
    runs-on: ubuntu-latest
    permissions:
      issues: write  # needed to post comments

    steps:
      - name: Answer issue with Invero
        env:
          INVERO_API_KEY: ${{ secrets.INVERO_API_KEY }}
          INVERO_PROJECT_ID: ${{ secrets.INVERO_PROJECT_ID }}
          INVERO_INTEGRATION_ID: ${{ secrets.INVERO_INTEGRATION_ID }}
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          ISSUE_NUMBER: ${{ github.event.issue.number }}
          ISSUE_TITLE: ${{ github.event.issue.title }}
          ISSUE_LABELS: ${{ join(github.event.issue.labels.*.name, ',') }}
          ISSUE_BODY: ${{ github.event.issue.body }}
          REPO: ${{ github.repository }}
        run: |
          pip install requests --quiet
          python3 << 'EOF'
          import os
          import requests
          import json
          import sys

          invero_api_key  = os.environ["INVERO_API_KEY"]
          invero_project  = os.environ["INVERO_PROJECT_ID"]
          invero_integration_id = os.environ["INVERO_INTEGRATION_ID"]
          gh_token      = os.environ["GH_TOKEN"]
          issue_number  = os.environ["ISSUE_NUMBER"]
          issue_title   = os.environ["ISSUE_TITLE"]
          issue_labels  = [l.strip().lower() for l in os.environ.get("ISSUE_LABELS", "").split(",") if l.strip()]
          issue_body    = os.environ.get("ISSUE_BODY", "")
          repo          = os.environ["REPO"]

          # --- 1. Build the question from the issue ---
          question = f"{issue_title}\n\n{issue_body}".strip()

          # --- 2. Check if it should skip ---
          labels_to_skip = ['bug', 'feature request']
          should_skip = any(label in issue_labels for label in labels_to_skip)
          if should_skip:
            sys.exit(0)

          # --- 3. Call the Invero Chat API ---
          invero_url = f"https://api.invero.ai/query/v1/projects/{invero_project}/chat/"
          invero_headers = {
              "X-API-KEY": invero_api_key,
              "Content-Type": "application/json",
          }
          invero_payload = {
              "query": question,
              "integration_id": invero_integration_id
          }

          invero_resp = requests.post(invero_url, headers=invero_headers, json=invero_payload, timeout=60)
          invero_resp.raise_for_status()
          invero_data = invero_resp.json()

          # Extract the answer text (adjust key if your Invero version differs)
          answer = invero_data.get("answer") or invero_data.get("message") or json.dumps(invero_data)

          # Optionally include relevant source links if Invero returns them
          sources = invero_data.get("relevant_sources", [])
          if sources:
              links = "\n".join(f"- [{s.get('title', s.get('url', ''))}]({s.get('url', '')})" for s in sources[:5])
              answer += f"\n\n**Relevant sources:**\n{links}"

          # --- 3. Post the answer as a GitHub issue comment ---
          comment_body = (
              f"๐Ÿ‘‹ **Invero bot here!** I found a potential answer to your question:\n\n"
              f"{answer}\n\n"
              f"---\n"
              f"*This answer was generated automatically. If it didn't help, a human will follow up.*"
          )

          gh_url = f"https://api.github.com/repos/{repo}/issues/{issue_number}/comments"
          gh_headers = {
              "Authorization": f"Bearer {gh_token}",
              "Accept": "application/vnd.github+json",
              "X-GitHub-Api-Version": "2022-11-28",
          }
          gh_resp = requests.post(gh_url, headers=gh_headers, json={"body": comment_body}, timeout=30)
          gh_resp.raise_for_status()

          print(f"โœ… Posted Invero answer to issue #{issue_number}")
          EOF

Breaking down the script

Triggering the action

This action triggers on issues.opened - anytime a new issue was opened within your repository.

on:
  issues:
    types

on:
  issues:
    types

on:
  issues:
    types

on:
  issues:
    types

This will only fire when a new issue is created, not when an issue is updated, edited, labeled or commented on.

Check the labels

There may be certain labels that you might not want an automated answer going out on - for example, 'bug' or 'feature request'. You can add as many labels to skip as you'd like with this line of code built into the Action:

labels_to_skip = ['bug', 'feature request']
labels_to_skip = ['bug', 'feature request']
labels_to_skip = ['bug', 'feature request']
labels_to_skip = ['bug', 'feature request']

Calling the Invero API

Now for the important part - calling the Invero API, giving it the context that it needs to generate a response. This part takes the query that's been generated by the issue title and body, along with the integration_id for your own analytics, and passes this to the /chat endpoint.

invero_url = f"https://api.invero.ai/query/v1/projects/{invero_project}/chat/"
invero_headers = {
  "X-API-KEY": invero_api_key,
  "Content-Type": "application/json",
}
invero_payload = {
  "query": question,
  "integration_id": invero_integration_id
}

invero_resp = requests.post(invero_url, headers=invero_headers, json=invero_payload, timeout=60)
invero_resp.raise_for_status()
invero_data = invero_resp.json()
invero_url = f"https://api.invero.ai/query/v1/projects/{invero_project}/chat/"
invero_headers = {
  "X-API-KEY": invero_api_key,
  "Content-Type": "application/json",
}
invero_payload = {
  "query": question,
  "integration_id": invero_integration_id
}

invero_resp = requests.post(invero_url, headers=invero_headers, json=invero_payload, timeout=60)
invero_resp.raise_for_status()
invero_data = invero_resp.json()
invero_url = f"https://api.invero.ai/query/v1/projects/{invero_project}/chat/"
invero_headers = {
  "X-API-KEY": invero_api_key,
  "Content-Type": "application/json",
}
invero_payload = {
  "query": question,
  "integration_id": invero_integration_id
}

invero_resp = requests.post(invero_url, headers=invero_headers, json=invero_payload, timeout=60)
invero_resp.raise_for_status()
invero_data = invero_resp.json()
invero_url = f"https://api.invero.ai/query/v1/projects/{invero_project}/chat/"
invero_headers = {
  "X-API-KEY": invero_api_key,
  "Content-Type": "application/json",
}
invero_payload = {
  "query": question,
  "integration_id": invero_integration_id
}

invero_resp = requests.post(invero_url, headers=invero_headers, json=invero_payload, timeout=60)
invero_resp.raise_for_status()
invero_data = invero_resp.json()

Posting the comment back to the issue

Once the response has been generated by Invero and returned to the Action, this will get posted in the thread. A helpful disclaimer is already built into the script above to clarify that the response is an automatically generated response in the event of the response not being sufficient, or a human needing to step in to add additional context.

Customisation ideas

There are a tonne of ways you could customise this to make it your own, here are some ideas:

  • Add a reaction before the comment is posted: use github.rest.reactions.createForIssue to add a ๐Ÿ‘€ emoji the moment the issue opens so users know that it's being looked at - even before Invero responds. Be wary of this if you are skipping responses based on labels, your reactions should take this into consideration, too!

  • Gate on issue length: consider adding a minimum issue length before triggering Invero - a query such as "help", "doesn't work" or "it's broken" likely won't generate a helpful response from Invero.

  • Close issues Invero answers confidently: Invero returns an is_uncertain attribute with it's responses, if this is false this means that Invero is confident the response was accurate, consider closing certain issues when this is set to false. Testing should be completed before rolling this out more widely.

Conclusion

With about 50 lines of YAML and JavaScript, you now have a self-service support layer built directly into your GitHub Issues workflow. Users get instant, documentation-grounded answers. Your team gets breathing room to focus on issues that actually need a human. And your docs get put to work in the most direct way possible: answering real questions from real users, in the exact moment they're asking.

The best part? Every issue that Invero handles successfully is a data point about what your users are struggling to find in your docs - a feedback loop that makes your documentation better over time.



Turn your companyโ€™s knowledge into AI agents