Slack Slash Commands via n8n
TL;DR — Create a Slack app, add a slash command pointing at an n8n webhook URL, verify signatures, respond within 3 seconds (or send a deferred response). One workflow per command. Pairs well with Jira / GitHub workflows from earlier in the month.
After auto-assigning Jira tickets, the next workflow pattern is Slack-initiated commands. Slash commands (/deploy staging, /jira PROJ-123, /oncall) are the right tool for “let any team member kick off a workflow without going to a separate UI.”
This post is the end-to-end: Slack app setup, n8n webhook configuration, signature verification, and a couple of useful command examples.
The shape of a slash command
When a user types /myapp arg1 arg2 in Slack:
- Slack sends a POST to your configured URL with the command + args + user info
- Your endpoint has 3 seconds to respond, or Slack shows “operation timed out”
- The immediate response is shown in the channel (or as ephemeral)
- Within 30 minutes, you can send up to 5 additional responses via
response_url
For workflows that take >3 seconds, the pattern is: respond immediately with “working on it…”, then post a follow-up when done.
Step 1: Create a Slack app
api.slack.com/apps → Create New App → From Scratch
App Name: Eng Bot
Workspace: pick yours
In the app:
- OAuth & Permissions: add Bot Token Scopes —
commands,chat:write,chat:write.public. (More scopes per command.) - Slash Commands: Create New Command
- Command:
/jira - Request URL: your n8n production webhook URL (set up below)
- Short Description: “Look up a Jira ticket”
- Usage Hint:
[KEY-123]
- Command:
- Basic Information: copy the Signing Secret (for verification)
- Install App: install to workspace, copy the Bot User OAuth Token
Step 2: n8n workflow
A Webhook trigger receives Slack’s POST:
HTTP Method: POST
Path: slack-jira
Response Mode: Respond to Webhook (don't auto-respond — we want control)
After the webhook, a Code node verifies Slack’s signature:
const crypto = require('crypto');
const item = $input.first();
const body = item.json.body;
const headers = item.json.headers;
const ts = headers['x-slack-request-timestamp'];
const sig = headers['x-slack-signature'];
// Reject if older than 5 minutes (replay protection)
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(ts, 10)) > 60 * 5) {
throw new Error('signature timestamp too old');
}
// Reconstruct the signed string
const rawBody = item.json.bodyRaw || $input.first().binary.body.toString(); // see note
const baseString = `v0:${ts}:${rawBody}`;
const expected = 'v0=' + crypto
.createHmac('sha256', $env.SLACK_SIGNING_SECRET)
.update(baseString)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
throw new Error('invalid signature');
}
// Parse the URL-encoded body Slack sends
const params = new URLSearchParams(rawBody);
return [{ json: Object.fromEntries(params) }];
Note: n8n’s webhook node by default parses JSON. For Slack slash commands (which send application/x-www-form-urlencoded), set the webhook node’s “Raw Body” option so you get the unparsed body needed for signature verification.
$env.SLACK_SIGNING_SECRET reads from the environment variable you set on the n8n container. Don’t put the secret in the workflow JSON.
After verification, the parsed body has fields like command, text, user_id, user_name, channel_id, response_url, team_id.
Step 3: Do the work
For a /jira PROJ-123 lookup:
const item = $input.first();
const text = item.json.text.trim();
const match = text.match(/^([A-Z]{2,10}-\d+)$/);
if (!match) {
return [{ json: {
response_type: 'ephemeral',
text: 'Usage: `/jira PROJ-123`',
}}];
}
return [{ json: { jira_key: match[1], response_url: item.json.response_url, user_name: item.json.user_name } }];
Then a Jira node to fetch the issue. Then a Code node to format the response:
const items = $input.all();
const issue = items[0].json;
return [{ json: {
response_type: 'in_channel',
text: `*<${issue.url}|${issue.key}>* — ${issue.fields.summary}\n` +
`Status: *${issue.fields.status.name}*\n` +
`Assignee: ${issue.fields.assignee?.displayName ?? 'Unassigned'}\n` +
`Priority: ${issue.fields.priority?.name ?? 'None'}`,
}}];
Step 4: Respond to Slack
Two response modes:
Immediate response (under 3 seconds total):
End the workflow with a “Respond to Webhook” node. Its body is what Slack shows:
Response Body: {{ JSON.stringify($json) }}
Response Headers: { "Content-Type": "application/json" }
This works when the whole workflow (verify + Jira call + format) finishes in 2.5 seconds.
Deferred response (workflow takes longer):
- Webhook node responds immediately with a placeholder (“Looking up…”)
- Workflow continues processing
- At the end, HTTP node POSTs the real response to
response_url
Pattern:
[Webhook]
↓
[Respond to Webhook: "Looking up..." (ephemeral)]
↓
[Code: verify signature]
↓
[Jira: fetch issue]
↓
[Code: format Slack message]
↓
[HTTP: POST to response_url with the real message]
The deferred POST goes to https://hooks.slack.com/... (the response_url from the slash command request). Slack replaces the placeholder.
response_type: in_channel vs ephemeral
in_channel — everyone in the channel sees it.
ephemeral — only the user who ran the command sees it.
Default is ephemeral. Use in_channel for things that the team should see (e.g., /deploy staging so everyone knows a deploy is happening). Use ephemeral for personal queries (/oncall showing your on-call schedule).
Useful commands for engineering teams
A few I’d recommend:
/jira KEY— lookup, link, status (what we built above)/oncall [team]— who’s on call right now, links to PagerDuty/deploy <service> <env>— kick off a deploy (with confirmation step)/pr <author>— PRs that user has open, awaiting review/sla [project]— current SLA status from your monitoring/snooze <jira-key> <duration>— defer ticket without losing it/runbook <service>— link to the service’s runbook
Each is its own workflow. n8n’s UI gets busy fast; group related workflows with naming convention (slash:jira, slash:deploy, etc.).
Common Pitfalls
Skipping signature verification. Anyone who knows your webhook URL can hit it. Always verify.
Hardcoding the signing secret in workflow JSON. Use env vars or n8n’s credentials. JSON-committed secrets are a leak.
Trying to do work synchronously when it takes >3 seconds. Slack times out. Use the deferred pattern.
Responding with in_channel for everything. Floods the channel. Default to ephemeral unless it’s genuinely team-visible.
Forgetting URL encoding when extracting from form-encoded body. Slack sends URL-encoded; URLSearchParams handles it. Don’t try to manually parse.
No rate limiting. A misused /deploy button can fire constantly. Add a Code-node rate limit (e.g., one deploy per user per minute) at the start.
Logging the full request payload. Includes user IDs and channel IDs; minor PII concern. Log only what you need for debugging.
Wrapping Up
Slack slash commands via n8n: app setup, signature verification, immediate + deferred responses. Lower the bar for team-initiated automation — anyone can run /oncall instead of digging into PagerDuty. Monday: Notion → Jira sync — the bidirectional sync pattern that handles knowledge tools talking to each other.