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:
A user opens a new GitHub Issue
A GitHub Actions workflow triggers on the `issues.opened` event
The workflow sends the issue title and body to the Invero API
Invero searches your ingested documentation and generates a relevant answer
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.
Go to your GitHub repository
Click Settings > Secrets and variables > Actions
Click New repository secret and add the following three secrets:
INVERO_PROJECT_ID - your Project ID
INVERO_INTEGRATION_ID - your Integration ID
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}")
EOFBreaking down the script
Triggering the action
This action triggers on issues.opened - anytime a new issue was opened within your repository.
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.