Connect Sentry and Linear to Slack
Verified August 11, 2026
Sentry knows what broke. Linear knows what you are doing about it. Nothing joins them, so someone reads the alert, searches Linear, finds nothing, and files the issue by hand.
This is one Slack bot that does the join. Ask it about an error and it reads the Sentry issue, searches Linear for a matching one, and files it with the stack trace and the release if there is none.
Both services have a Slack app already. Neither can do this, because neither can see the other.
What you need
- A Slack workspace where you can install apps
- A Sentry account and a Linear account
- An OpenRouter account
Install the CLI
npm i -g @substructure.ai/cliWrite the config
Save this as substructure.toml.
name = "triage-bot"
[llm.openrouter]
type = "openrouter"
[mcp.sentry]
url = "https://mcp.sentry.dev/mcp"
[mcp.linear]
url = "https://mcp.linear.app/mcp"
[agent.triage]
llm = "openrouter"
model = "z-ai/glm-5.2"
system = """
You are the triage assistant. When asked about an error, read it in Sentry, then
search Linear for an existing issue about it. File one only if none exists, and
include the stack trace and the affected release. Link the Linear issue in your
reply.
"""
mcp = [
{ id = "sentry", tools = { read_only = true } },
{ id = "linear", tools = { include = ["*issue*", "*search*", "*create*"] } },
]
[slack]
dm = "triage"
mentions = "triage"
[remote]
url = "https://api.substructure.ai"[agent.triage] declares the agent: its model, its prompt, and the connections it can reach. The Slack bot is how you talk to it.
The file holds two server URLs and the agent that uses both. It holds no tokens.
[remote] is the deployment the CLI talks to. subs apply writes the org and the project back under it.
Create the project
subs applyThis creates the project from the file and writes the project id back into it. If you are not signed in yet, the command opens your browser first. Run subs apply again after any edit to the file.
Add your LLM key
subs llm set-key openrouterThe agent's model runs through OpenRouter, so it needs an OpenRouter API key. Create one at openrouter.ai/keys if you do not have one. The command stores the key with your deployment. It never goes in the config file.
Authorize Sentry and Linear
A credential belongs to a connection id, so each one is authorized on its own.
subs mcp login sentry
subs mcp login linearEach command opens that service's consent page. Approve it and the credential goes to your deployment.
Run subs mcp list to check both connections are authorized. A declared connection with no credential reaches nothing, and the bot will answer from one service while silently failing at the other.
Connect Slack
subs slack connectPick your workspace and approve. The bot is live once the command prints the workspace name.
Ask it something
Invite the bot to a channel and mention it.
- "what is breaking in checkout, and is it tracked?"
- "triage the top error from last night"
- "file the null plan error if nobody has"
Each tool call shows in the thread as a task card. A single answer usually spans several: one or two into Sentry, one search in Linear, and a create if the search came back empty. Open a card to see the arguments and the result.
Read one, write the other
The filter belongs to the agent, not to the connection. That is what makes the asymmetry possible.
mcp = [
{ id = "sentry", tools = { read_only = true } },
{ id = "linear", tools = { include = ["*issue*", "*search*", "*create*"] } },
]Sentry is read-only, so the bot can never resolve or assign an error there by mistake. Linear takes an include list rather than read_only, because filing is the point.
The filter is also what keeps the bot working. A model picks worse as the tool list grows, and worst between tools that look alike, which is exactly what two full servers give it. Naming an id on its own takes every tool it offers.
Because the filter is per agent, one Sentry connection can serve two agents that see different tools. A read-only agent in a public channel and a wider one in your own DMs share a single credential.
Tool names carry their connection
Both servers offer a search. Tools arrive prefixed with the connection id, so they stay distinct.
sentry__search_issues
linear__search_issuesThe system prompt is where you say which to reach for first. Without it a model will sometimes search Linear for the error text and answer that nothing is wrong.
If it does not answer
- One connection not authorized. Run
subs mcp list. A bot with Sentry but not Linear reads the error and files nothing. - No
[slack]section. A connected workspace with no[slack]never replies. - Bot not invited. Invite it to the channel before you mention it.
- It answers but never files. Check the Linear filter is not
read_only, and that the include list covers the create tool. - Tools missing. The engine fetches the tool list once per session. Start a new thread after you change a filter.
- Anything else. Run
subs doctor.
Add a third service
Declare it and name it on the agent. GitHub turns triage into the full chain: the error, the pull request that introduced it, and the issue that tracks it.
[mcp.github]
url = "https://api.githubcopilot.com/mcp/"
auth = "token"
[agent.triage]
mcp = [
{ id = "sentry", tools = { read_only = true } },
{ id = "linear", tools = { include = ["*issue*", "*search*", "*create*"] } },
{ id = "github", tools = { include = ["*pull_request*", "*commit*"] } },
]GitHub takes a static token rather than OAuth, so it is authorized with subs mcp set-token github instead of subs mcp login. Three servers is where filtering stops being optional.
Next steps
- Connect a Slack bot to Sentry or to Linear, one service at a time.
- Run the bot on an open model: prices, context, and what a thread costs.
- How MCP connectors work: auth, tool fetching, and filters.
- Per-channel agents: point one channel at this bot and another somewhere else.
- Pricing: one flat price a month, and no per-token charges from us.