How I Built Helm, an AI Agent That Actually Solves Outages
Code: https://github.com/RajdeepKushwaha5/HELM
Demo video: https://drive.google.com/drive/u/0/folders/1elL9cQ86p_-D2-Jt_lULuX42pzNLlSKj
Build time: about an hour once your tokens are ready. Everything below is real and runnable.
It was 3 AM the first time I really understood the problem.
A service was down. The pager would not stop. Slack was a wall of red text and question marks. Sentry was throwing the same fatal error a thousand times a minute. And everyone in the channel was asking the one question nobody could answer fast enough: which pull request did this?
I had GitHub open in one tab to scroll recent merges. Sentry in another to read the stack trace. PagerDuty in a third to see who got paged. Slack in a fourth to follow the thread. A fifth tab for the deploy log. I was a human JOIN, sitting there matching timestamps in my head while the clock ran and the outage cost went up.
We found the PR eventually. It took about forty minutes. Forty minutes of five people staring at five tabs, because the one fact we needed, this merge caused that crash, did not live in any single tool. It lived in the space between them, and none of those tools were built to talk to each other.
That night stuck with me. So when the hackathon came around, I decided to build the thing I wished I’d had at 3 AM. I called it Helm.
The pitch is simple. You ask one question in plain English. Helm runs a single SQL query across GitHub, Sentry, PagerDuty, Linear, Slack and CircleCI, and hands you the answer with the exact query attached so you can check its work. No tab-hopping. No detective work.
Here is the exact route I took. If you want to build your own, follow along. I am giving you the real SQL, the real source spec, and the real backend code, not a hand-wave.
The bet
The whole thing rests on one bet: what if all my tools were just one database?
Not a data warehouse. I did not want to copy gigabytes of GitHub and Sentry data into some Postgres instance and keep it in sync forever. I wanted to query the live APIs directly, as if they were tables, and join across them in one statement.
That is exactly what Coral does. It is a federated SQL engine that turns live SaaS APIs into SQL tables. You write SELECT, it makes the API calls, joins the results in memory, and hands you rows. Nothing is copied. So the first thing I did was stop writing app code and go prove the bet in a terminal.
Act 1: The first SELECT
Coral runs locally. On Windows I run it inside WSL. You install it, then connect your tools one at a time. Coral asks for each credential once and locks it in a local secret store, never in your project.
# install (macOS/Linux, or inside WSL on Windows)
brew install withcoral/tap/coral
# connect the core engineering tools
coral source add --interactive github
coral source add --interactive sentry
coral source add --interactive pagerduty
coral source add --interactive linear
coral source add --interactive slack
# prove the engine is alive
coral sql "SELECT schema_name, COUNT(*) AS tables
FROM coral.tables GROUP BY 1 ORDER BY 1"
That last line reads Coral’s own catalog. My sources showed up with their table counts, and GitHub alone exposed hundreds of tables. Then I ran the one that made me grin:
SELECT number, title, user__login, merged_at
FROM github.pulls
WHERE owner = 'myorg' AND repo = 'myrepo' AND state = 'closed'
LIMIT 5
Live pull requests, in milliseconds, in a terminal, as a table. No SDK, no pagination loop, no glue code. The bet was looking good.
Try this before anything else. Run that query against your own repo. If rows come back, the engine works and you are ready to build the actual product. If they do not, fix Coral before writing a single line of app code. Prove the data layer first.
Act 2: The join that made me sit back
This is the moment the product became real. I wanted the answer to the 3 AM question, which PR broke production, expressed as one query. The logic is almost embarrassingly simple once the tools are tables: a PR merged, and within a day an error first appeared. Match them on the time window.
SELECT
g.number AS pr_number,
g.title AS pr_title,
g.user__login AS author,
g.merged_at,
s.title AS error_title,
s.level,
s.count AS times_seen
FROM github.pulls g
JOIN sentry.issues s
ON CAST(s.first_seen AS TIMESTAMP) >= CAST(g.merged_at AS TIMESTAMP)
AND CAST(s.first_seen AS TIMESTAMP) <= CAST(g.merged_at AS TIMESTAMP) + INTERVAL '24 hours'
WHERE g.owner = 'myorg' AND g.repo = 'myrepo'
AND g.state = 'closed'
AND s.level IN ('error', 'fatal')
AND CAST(g.merged_at AS TIMESTAMP) >= CAST(CURRENT_DATE AS TIMESTAMP) - INTERVAL '30 days'
ORDER BY g.merged_at DESC, s.count DESC
I ran it. It returned the PR. The exact one. The thing that took five people forty minutes was now one query that ran in seconds. I actually sat back in my chair.
Then I got greedy and walked the whole chain, PR to error to incident to the Slack thread where everyone was panicking:
SELECT
pr.title AS pr_title,
s.title AS error_title,
pd.id AS incident_id,
COUNT(sl.text) AS slack_messages
FROM github.pulls pr
JOIN sentry.issues s
ON CAST(s.first_seen AS TIMESTAMP)
BETWEEN CAST(pr.merged_at AS TIMESTAMP)
AND CAST(pr.merged_at AS TIMESTAMP) + INTERVAL '24 hours'
LEFT JOIN pagerduty.incidents pd
ON pd.service__summary ILIKE '%' || s.project || '%'
LEFT JOIN slack.messages(channel => 'C0123456789') sl
ON CAST(sl.ts AS TIMESTAMP)
BETWEEN CAST(pd.created_at AS TIMESTAMP)
AND CAST(pd.created_at AS TIMESTAMP) + INTERVAL '4 hours'
WHERE pr.owner = 'myorg' AND pr.repo = 'myrepo' AND pr.state = 'closed'
GROUP BY pr.title, s.title, pd.id
ORDER BY slack_messages DESC
Four companies’ APIs, one query, the entire incident on a single row.
Act 3: The wall (and the gotchas that cost me an hour)
It was not all smooth. I hit a wall, and I want you to hit it for ten seconds instead of an hour, so here are the three things that broke my queries:
- Nested fields use a double underscore. The API field user.login is user__login in Coral. The commit SHA head.sha is head__sha. I spent a while confused about "missing columns" that were just named differently.
- Timestamps are strings. Coral returns ISO-8601 text, so you must CAST(x AS TIMESTAMP) before any interval math. Every time-based join above does this.
- Slack messages are a table function, not a table. You call slack.messages(channel => '...') with that arrow syntax. There is no plain slack.messages table to SELECT * from.
Once those three clicked, everything I tried just worked. And I learned to prefer exact join keys over fuzzy text wherever possible. My favorite is linking a PR to its Linear ticket on the exact attachment URL, which gives zero false positives:
FROM github.pulls g
JOIN linear.attachments la ON la.url = g.html_url
JOIN linear.issues li ON li.id = la.issue_id
Act 4: Teaching Coral a new trick (a custom source)
Coral ships connectors for the big tools, but I wanted to answer a question that needs CI data: which PRs passed CI green but still crashed production? That gap is invisible to every tool, because CircleCI only knows pass or fail, and Sentry only knows what broke. The bridge between them is the commit SHA.
So I wrote my own Coral source. A source is just a folder with a manifest.yaml. You declare the base URL, the auth, the tables, and how each column maps out of the response JSON. Here is the shape:
name: circleci
dsl_version: 3
backend: http
base_url: https://circleci.com/api/v2
inputs:
CIRCLECI_TOKEN:
kind: secret
hint: CircleCI personal API token from app.circleci.com/settings/user/tokens
auth:
type: HeaderAuth
headers:
- name: Circle-Token
from: input
key: CIRCLECI_TOKEN
tables:
- name: pipelines
filters:
- name: project_slug
required: true
request:
method: GET
path: /project/{{filter.project_slug}}/pipeline
response:
rows_path: [items]
pagination:
mode: cursor_query
response_cursor_path: [next_page_token]
cursor_param: page-token
columns:
- name: vcs_revision
type: Utf8
description: Git commit SHA. JOIN with github.pulls.head__sha to link PRs to CI runs.
expr: { kind: path, path: [vcs, revision] }
- name: state
type: Utf8
expr: { kind: path, path: [state] }
Install it with coral source add --file sources/circleci/manifest.yaml, and CircleCI is now SQL. The question I could not answer before becomes a query:
SELECT g.number, g.title, c.state AS ci_state, s.title AS error_title
FROM github.pulls g
JOIN circleci.pipelines c ON c.vcs_revision = g.head__sha
JOIN sentry.issues s
ON CAST(s.first_seen AS TIMESTAMP)
BETWEEN CAST(g.merged_at AS TIMESTAMP)
AND CAST(g.merged_at AS TIMESTAMP) + INTERVAL '24 hours'
WHERE c.state != 'errored' AND s.level IN ('error','fatal')
Green build, broken production, on one row. The moment I could write a source in an afternoon, I stopped thinking of Coral as a fixed set of connectors and started thinking of it as a language for turning any API into a table.
Act 5: A thin backend that never lies
Here is a principle I held to the whole build: the backend reimplements nothing. It runs Coral and packages the answer with its own proof. Every result carries the SQL it ran, the sources it touched, the row count, and the runtime.
import subprocess, json
def coral_query(sql: str) -> dict:
proc = subprocess.run(
[CORAL_BIN, "sql", "--format", "json", sql],
capture_output=True, text=True, timeout=30,
)
ok = proc.returncode == 0
return {
"rows": json.loads(proc.stdout) if ok else [],
"sql": sql,
"status": "ok" if ok else "error",
"error": None if ok else proc.stderr.strip(),
}
Two decisions made it feel production-grade. First, I run a page’s queries in parallel with a ThreadPoolExecutor so the UI is not waiting on subprocesses one at a time:
from concurrent.futures import ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=4) as pool:
futures = {name: pool.submit(coral_query, sql) for name, sql in page_queries.items()}
results = {name: f.result() for name, f in futures.items()}
Second, Coral is SELECT only at query time, so the agent physically cannot mutate anything. Any action it proposes, a Slack note, a Linear ticket, a rollback comment, is drafted and held for a human to approve. An incident agent that can accidentally write to your tools is a liability. This one can only ever read and suggest.
Act 6: Ask Helm, plain English to a live JOIN
This is the part demos remember. Most agents are handed fifty tools (search_github, search_sentry, and so on) and left to guess which to call in which order. I gave the model one job instead: write Coral SQL.
The trick that made it genuinely good was schema pruning. Before I send the question to the model, I fetch the schema of only the sources that question is about, not all of them. Dumping every column of every source wastes tokens and makes the query plans worse.
# fetch only the relevant schema, then ground the model on it
schema = coral_query(f"""
SELECT schema_name, table_name, column_name
FROM coral.columns
WHERE schema_name IN ({active_sources})
""")
prompt = f"CORAL SCHEMA:\n{schema}\n\nQUESTION: {question}\nWrite one Coral SQL query."
The model writes the query, the backend runs it, and the answer arrives with the generated SQL attached. I keep a small session store too, so a follow-up like “what is the author’s deploy error rate?” knows who the author is from the previous turn. Multi-turn memory, backed by real SQL.
A normal tool-loop agent would make dozens of sequential API calls and burn a huge token budget to answer the 3 AM question. Helm plans one cross-source query and runs it.
Act 7: Mission Control, and the one rule
Nobody wants to type SQL at 3 AM, so I wrapped it in a React and Vite dashboard built to feel like a command center. The anchor is the Live Monitor: the merged PR on the left, the Sentry error on the right, and the live SQL join in the middle that ties them together.
I held to one rule the whole way: no chart without its query. Every panel has a “View SQL” toggle that shows the exact statement, the sources, the row count, and the runtime. When a teammate or a judge asks “how do you know that?”, the answer is right there on screen. The pitch is not “trust the dashboard.” It is “here is the query, run it yourself.”
# backend
cd helm/backend && pip install -r requirements.txt && uvicorn main:app --port 8000
# frontend
cd helm/frontend && npm install && npm run dev # http://localhost:5173
How the story ends
I tested Helm on a simulated outage, the same kind of mess that cost us forty minutes that night. It found the exact PR in about four seconds, then answered a follow-up about the author’s overdue tickets without me repeating myself. The 3 AM scramble became a sentence and a couple of clicks.
If you take one thing from this post, build Act 2. Open the Coral CLI and write the PR-to-error join against your own repo. The instant it returns a real row, you will feel what I felt in that chair. Coral does the genuinely hard part, making many systems queryable as one. The backend, the UI, the agent, all of it is just presentation on top of SQL.
See it run: https://drive.google.com/drive/u/0/folders/1elL9cQ86p_-D2-Jt_lULuX42pzNLlSKj
Grab the code and build your own: https://github.com/RajdeepKushwaha5/HELM
Run Coral locally, write the join, and you will never look at five tabs the same way again.