GitHub Actions Integration
Scitorβs automatic labels make it easy to build powerful automations with GitHub Actions. This guide covers common patterns for automating your support workflow.
Auto-assign based on category
Automatically assign team members based on the type of support request:
name: Auto-assign support tickets
on:
issues:
types: [labeled]
jobs:
assign:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
const label = context.payload.label.name;
const assignments = {
'category:bug-report': ['dev-team-lead'],
'category:billing': ['billing-team'],
'category:account': ['account-manager'],
'category:feature-request': ['product-manager'],
};
const assignees = assignments[label];
if (assignees) {
await github.rest.issues.addAssignees({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
assignees,
});
}
Auto-close spam
Automatically close issues with high spam scores:
name: Auto-close spam
on:
issues:
types: [labeled]
jobs:
close-spam:
if: github.event.label.name == 'spam:high'
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
state: 'closed',
state_reason: 'not_planned',
});
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: 'π€ Automatically closed β high spam score detected.',
});
Slack notification for urgent issues
Send a Slack notification when a frustrated customer reports a bug:
name: Urgent issue alert
on:
issues:
types: [labeled]
jobs:
notify:
runs-on: ubuntu-latest
steps:
- name: Check if urgent
id: check
uses: actions/github-script@v7
with:
script: |
const labels = context.payload.issue.labels.map(l => l.name);
const isNegative = labels.includes('sentiment:negative');
const isBug = labels.includes('category:bug-report');
core.setOutput('urgent', isNegative && isBug ? 'true' : 'false');
- name: Send Slack notification
if: steps.check.outputs.urgent == 'true'
uses: slackapi/slack-github-action@v2
with:
webhook: ${{ secrets.SLACK_WEBHOOK }}
webhook-type: incoming-webhook
payload: |
{
"text": "π¨ Urgent support issue",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*π¨ Urgent: Frustrated customer bug report*\n<${{ github.event.issue.html_url }}|${{ github.event.issue.title }}>"
}
}
]
}
SLA tracking
Add a label if an issue hasnβt been responded to within a time period:
name: SLA check
on:
schedule:
- cron: '0 */4 * * *' # Every 4 hours
jobs:
check-sla:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
const issues = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
labels: 'spam:clean',
sort: 'created',
direction: 'asc',
per_page: 50,
});
const fourHoursAgo = new Date(Date.now() - 4 * 60 * 60 * 1000);
for (const issue of issues.data) {
if (new Date(issue.created_at) < fourHoursAgo && issue.comments === 0) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: ['sla:breached'],
});
}
}
Tip
These are starting points β customize the logic, team members, and thresholds to match your teamβs workflow. GitHub Actions gives you full flexibility to build exactly the automation you need.
Auto-acknowledge inbound emails
Send an automatic thank-you reply to every non-spam inbound email. The workflow triggers when Scitor labels the issue spam:clean, posts a /send comment, and Scitor emails it back to the customer:
name: Auto-acknowledge inbound emails
on:
issues:
types: [labeled]
jobs:
acknowledge:
# Only reply to non-spam issues created by Scitor
if: >
github.event.label.name == 'spam:clean' &&
github.event.issue.user.login == 'scitor-customerops[bot]'
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
// Avoid double-replying if labels are re-applied
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
per_page: 5,
});
const alreadyReplied = comments.data.some(
c => c.user.login === 'github-actions[bot]' && c.body.includes('/send')
);
if (alreadyReplied) return;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: [
'/send',
'',
'Thank you for reaching out! We have received your message and a member of our team will get back to you as soon as possible.',
'',
'In the meantime, you may find answers in our documentation at [support.scitor.io](https://support.scitor.io).',
'',
'Kind regards,',
'The Support Team',
].join('\n'),
});
Tip
This triggers on spam:clean only. If you also want to acknowledge spam:low emails, change the condition to check both:
if: >
(github.event.label.name == 'spam:clean' || github.event.label.name == 'spam:low') &&
github.event.issue.user.login == 'scitor-customerops[bot]'
The duplicate-check ensures the email isnβt sent twice if labels are reapplied.
Auto-reply to common questions
Send an automated first response for questions. The /send command tells Scitor to email the reply back to the original sender:
name: Auto-reply to questions
on:
issues:
types: [labeled]
jobs:
auto-reply:
if: github.event.label.name == 'category:question'
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: [
"/send",
"",
"π Thanks for reaching out! We've received your question and will get back to you shortly.",
"",
"In the meantime, you might find the answer in our documentation:",
"- [Getting Started](https://docs.scitor.io/your-org/support/getting-started/)",
"- [FAQ](https://docs.scitor.io/your-org/support/faq/)",
"",
"Our team typically responds within 4 business hours."
].join('\n'),
});
Tip
The /send command at the start of the comment tells Scitor to email the message to the original sender. Itβs automatically stripped from the email body. This works from GitHub Actions workflows β Scitor recognizes commands from github-actions[bot].
Escalate billing issues
Automatically add priority labels and notify a specific team when billing or account issues come from frustrated customers:
name: Escalate billing issues
on:
issues:
types: [labeled]
jobs:
escalate:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
const labels = context.payload.issue.labels.map(l => l.name);
const isBilling = labels.includes('category:billing') || labels.includes('category:account');
const isNegative = labels.includes('sentiment:negative');
if (isBilling && isNegative) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['priority:high'],
});
await github.rest.issues.addAssignees({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
assignees: ['billing-lead'],
});
}
Add to GitHub Projects board
Automatically add new support issues to a GitHub Projects board and set the status column based on category:
name: Add to support board
on:
issues:
types: [opened]
jobs:
add-to-project:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
const projectNumber = 1; // Your project number
const org = context.repo.owner;
// Get project ID
const project = await github.graphql(`
query($org: String!, $number: Int!) {
organization(login: $org) {
projectV2(number: $number) {
id
}
}
}
`, { org, number: projectNumber });
// Add issue to project
await github.graphql(`
mutation($projectId: ID!, $contentId: ID!) {
addProjectV2ItemById(input: {
projectId: $projectId,
contentId: $contentId
}) {
item { id }
}
}
`, {
projectId: project.organization.projectV2.id,
contentId: context.payload.issue.node_id,
});
Weekly support digest
Generate a weekly summary of support activity and post it as a discussion or issue:
name: Weekly support digest
on:
schedule:
- cron: '0 9 * * 1' # Every Monday at 9 AM
jobs:
digest:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
const oneWeekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
const issues = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
since: oneWeekAgo,
state: 'all',
per_page: 100,
});
const opened = issues.data.filter(i => !i.pull_request && new Date(i.created_at) > new Date(oneWeekAgo));
const closed = issues.data.filter(i => !i.pull_request && i.state === 'closed');
const byCategory = {};
const bySentiment = {};
for (const issue of opened) {
for (const label of issue.labels) {
const name = typeof label === 'string' ? label : label.name;
if (name.startsWith('category:')) byCategory[name] = (byCategory[name] || 0) + 1;
if (name.startsWith('sentiment:')) bySentiment[name] = (bySentiment[name] || 0) + 1;
}
}
const categoryLines = Object.entries(byCategory)
.sort((a, b) => b[1] - a[1])
.map(([k, v]) => `- ${k}: ${v}`)
.join('\n') || '- No categorized issues';
const sentimentLines = Object.entries(bySentiment)
.sort((a, b) => b[1] - a[1])
.map(([k, v]) => `- ${k}: ${v}`)
.join('\n') || '- No sentiment data';
const body = [
`# π Weekly Support Digest`,
`*${new Date(oneWeekAgo).toLocaleDateString()} β ${new Date().toLocaleDateString()}*`,
``,
`## Overview`,
`- **New issues:** ${opened.length}`,
`- **Closed issues:** ${closed.length}`,
`- **Open rate:** ${opened.length > 0 ? Math.round(closed.length / opened.length * 100) : 0}% resolved`,
``,
`## By category`,
categoryLines,
``,
`## By sentiment`,
sentimentLines,
].join('\n');
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `π Weekly Support Digest β ${new Date().toLocaleDateString()}`,
body,
labels: ['digest'],
});
Auto-block spam and close
Automatically block the sender and close the issue when Scitor labels it as high spam:
name: Auto-block spam
on:
issues:
types: [labeled]
jobs:
block-spam:
if: github.event.label.name == 'spam:high'
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
// Block the sender using Scitor's /block-sender command
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: '/block-sender',
});
// Close the issue
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
state: 'closed',
state_reason: 'not_planned',
});
Apply priority labels by sentiment
Add priority labels based on the combination of sentiment and category:
name: Priority labels
on:
issues:
types: [labeled]
jobs:
prioritize:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
const labels = context.payload.issue.labels.map(l => l.name);
// Determine priority based on label combinations
let priority = null;
if (labels.includes('sentiment:negative') && labels.includes('category:bug-report')) {
priority = 'priority:urgent';
} else if (labels.includes('sentiment:negative')) {
priority = 'priority:high';
} else if (labels.includes('category:bug-report')) {
priority = 'priority:medium';
} else if (labels.includes('category:feature-request')) {
priority = 'priority:low';
}
if (priority) {
// Remove any existing priority labels first
for (const label of labels) {
if (label.startsWith('priority:')) {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
name: label,
}).catch(() => {}); // Ignore if already removed
}
}
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: [priority],
});
}
External integrations
Because support tickets are GitHub Issues, any API call you can make from a workflow file is an integration. No Zapier subscription or vendor-built connector needed β a workflow file, a repository secret, and a fetch() call connects your support inbox to any service with an HTTP endpoint.
Post to any webhook
Send ticket data to Zapier, Make, your own backend, or any service that accepts an incoming webhook. This is the most general pattern: receive the event wherever you want, then handle it there.
The workflow fires once per new ticket when Scitor applies spam:clean β meaning the message has already been AI-triaged and is a real inbound request:
name: Ticket webhook
on:
issues:
types: [labeled]
jobs:
send-webhook:
if: >
github.event.label.name == 'spam:clean' &&
github.event.issue.user.login == 'scitor-customerops[bot]'
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
const labels = context.payload.issue.labels.map(l => l.name);
const payload = {
event: 'ticket.created',
ticket: {
number: context.issue.number,
title: context.payload.issue.title,
url: context.payload.issue.html_url,
created_at: context.payload.issue.created_at,
category: labels.find(l => l.startsWith('category:'))?.replace('category:', '') ?? null,
sentiment: labels.find(l => l.startsWith('sentiment:'))?.replace('sentiment:', '') ?? null,
priority: labels.find(l => l.startsWith('priority:'))?.replace('priority:', '') ?? null,
},
};
const res = await fetch(process.env.WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!res.ok) core.setFailed(`Webhook returned ${res.status}`);
env:
WEBHOOK_URL: ${{ secrets.WEBHOOK_URL }}
Add WEBHOOK_URL as a repository secret. Point it at a Zapier Catch Hook, a Make webhook, or your own endpoint β the ticket lands as JSON and you route it anywhere from there.
Slack: new ticket feed
Notify a Slack channel for every clean incoming ticket, with category and sentiment surfaced in the message. Unlike the urgent-only example above, this fires for every new ticket and works well as a general support-inbox feed:
name: New ticket Slack feed
on:
issues:
types: [labeled]
jobs:
slack:
if: >
github.event.label.name == 'spam:clean' &&
github.event.issue.user.login == 'scitor-customerops[bot]'
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
id: meta
with:
script: |
const labels = context.payload.issue.labels.map(l => l.name);
const category = labels.find(l => l.startsWith('category:'))?.replace('category:', '') ?? 'uncategorized';
const sentiment = labels.find(l => l.startsWith('sentiment:'))?.replace('sentiment:', '') ?? 'unknown';
const priority = labels.find(l => l.startsWith('priority:'))?.replace('priority:', '');
const emoji = { positive: 'π’', neutral: 'π‘', negative: 'π΄' }[sentiment] ?? 'βͺ';
core.setOutput('category', category);
core.setOutput('emoji', emoji);
core.setOutput('priority', priority ? ` Β· *${priority}*` : '');
- uses: slackapi/slack-github-action@v2
with:
webhook: ${{ secrets.SLACK_WEBHOOK }}
webhook-type: incoming-webhook
payload: |
{
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "${{ steps.meta.outputs.emoji }} *${{ steps.meta.outputs.category }}*${{ steps.meta.outputs.priority }}\n<${{ github.event.issue.html_url }}|${{ github.event.issue.title }}>"
},
"accessory": {
"type": "button",
"text": { "type": "plain_text", "text": "View ticket" },
"url": "${{ github.event.issue.html_url }}"
}
}
]
}
Microsoft Teams notification
The same notification using a Teams Incoming Webhook. Cards are color-coded by sentiment β green for positive, amber for neutral, red for negative:
name: New ticket Teams notification
on:
issues:
types: [labeled]
jobs:
teams:
if: >
github.event.label.name == 'spam:clean' &&
github.event.issue.user.login == 'scitor-customerops[bot]'
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
const labels = context.payload.issue.labels.map(l => l.name);
const category = labels.find(l => l.startsWith('category:'))?.replace('category:', '') ?? 'Uncategorized';
const sentiment = labels.find(l => l.startsWith('sentiment:'))?.replace('sentiment:', '') ?? 'unknown';
const color = { positive: '00B870', neutral: 'F5A623', negative: 'D63C1E' }[sentiment] ?? 'B0B8C8';
await fetch(process.env.TEAMS_WEBHOOK, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
'@type': 'MessageCard',
'@context': 'https://schema.org/extensions',
themeColor: color,
summary: context.payload.issue.title,
sections: [{
activityTitle: `New support ticket β ${category}`,
activitySubtitle: context.payload.issue.title,
facts: [
{ name: 'Sentiment', value: sentiment },
{ name: 'Opened', value: new Date(context.payload.issue.created_at).toUTCString() },
],
}],
potentialAction: [{
'@type': 'OpenUri',
name: 'View ticket',
targets: [{ os: 'default', uri: context.payload.issue.html_url }],
}],
}),
});
env:
TEAMS_WEBHOOK: ${{ secrets.TEAMS_WEBHOOK }}
File a Linear issue from a bug report
When Scitor labels a ticket category:bug-report, create a corresponding issue in Linear and post the URL back to the support ticket:
name: Bug report β Linear
on:
issues:
types: [labeled]
jobs:
linear:
if: github.event.label.name == 'category:bug-report'
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
const description = [
`Customer bug report: ${context.payload.issue.html_url}`,
'',
context.payload.issue.body?.substring(0, 500) ?? '',
].join('\n');
const res = await fetch('https://api.linear.app/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': process.env.LINEAR_API_KEY,
},
body: JSON.stringify({
query: `
mutation($title: String!, $desc: String!, $teamId: String!) {
issueCreate(input: { title: $title, description: $desc, teamId: $teamId }) {
issue { id url }
}
}
`,
variables: {
title: `[Customer] ${context.payload.issue.title}`,
desc: description,
teamId: process.env.LINEAR_TEAM_ID,
},
}),
});
const { data } = await res.json();
const linearUrl = data?.issueCreate?.issue?.url;
if (linearUrl) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `π Linear: ${linearUrl}`,
});
}
env:
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
LINEAR_TEAM_ID: ${{ secrets.LINEAR_TEAM_ID }}
PagerDuty alert for critical tickets
Trigger a PagerDuty incident when a ticket is escalated to priority:urgent:
name: PagerDuty β priority urgent
on:
issues:
types: [labeled]
jobs:
pagerduty:
if: github.event.label.name == 'priority:urgent'
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
await fetch('https://events.pagerduty.com/v2/enqueue', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
routing_key: process.env.PAGERDUTY_ROUTING_KEY,
event_action: 'trigger',
dedup_key: `scitor-${context.issue.number}`,
payload: {
summary: `Critical customer issue: ${context.payload.issue.title}`,
severity: 'critical',
source: 'scitor-support',
custom_details: {
url: context.payload.issue.html_url,
issue_number: context.issue.number,
},
},
links: [{ href: context.payload.issue.html_url, text: 'View support ticket' }],
}),
});
env:
PAGERDUTY_ROUTING_KEY: ${{ secrets.PAGERDUTY_ROUTING_KEY }}
Tip
dedup_key prevents duplicate PagerDuty incidents if the priority:urgent label is removed and reapplied on the same ticket. Using the issue number as a stable key ensures a single incident per ticket.
Was this article helpful?