Building a Slack AI Agent That Can Actually Query Your Warehouse
A practitioner’s guide to shipping a Slack AI agent that runs real warehouse queries—scoped, grounded on dbt, cost-safe, and evaluated for drift.
Your leaders want answers where work already happens, and they want them fast—ask in Slack, get real numbers from the warehouse. You can ship that. The blueprint: a least-privilege Slack app with OAuth, an agent loop with tool calls, warehouse access that’s read-only by default, query cost guards, and human-in-the-loop for anything that writes. Ground the agent on your dbt docs and semantic layer so it uses your definitions, not the model’s guesses. Run evals to spot drift. Below is the architecture we deploy for clients and the exact pieces you can copy. If you want help standing it up or hardening it for production threads, see our service overview for Slack AI agents built for analytics.
Architecture: scopes, OAuth, and the agent loop
Start with a Slack app in your org. Use OAuth and only the scopes you truly need. In most teams, we use a bot user and these reads/writes for threads where the agent operates inside one workspace:
app_mentions:read,channels:history,groups:history,im:historychat:write,commands,users:read,team:read- Optional:
files:writefor chart attachments
Use the Web API via OAuth V2; the Slack Developer Docs list exact meanings. Keep tokens in a secret manager, not in code. A simple agent loop looks like this:
# Python (FastAPI or Flask behind a Slash command or event subscription)
# Pseudocode: message -> plan -> tools -> SQL -> result -> summary
def handle_message(event):
q = normalize(event.text)
intent = classify(q)
ctx = retrieve_docs(q) # dbt docs, glossary, data contracts
plan = propose_plan(q, ctx)
for step in plan:
if step.tool == "query":
sql = text_to_sql(q, ctx)
sql = enforce_limits(sql)
rows = run_sql(sql)
audit_log(event.user, sql, rows)
elif step.tool == "chart":
img = render_chart(rows)
elif step.tool == "request_approval":
enqueue_human_review(event.channel, step.payload)
answer = compose_answer(q, rows, ctx)
return post_message(event.channel, answer, thread_ts=event.ts)
The loop is simple on paper; the hard parts are tool design, safe SQL, and grounding so the agent doesn’t invent metrics. We’ll tackle those next.
Warehouse access that can’t burn you
Give the agent read-only credentials and a role that touches only curated schemas. Example Snowflake setup:
-- Snowflake: create a minimal read role
CREATE ROLE AI_AGENT_READ;
GRANT USAGE ON WAREHOUSE ANALYTICS_WH TO ROLE AI_AGENT_READ;
GRANT USAGE ON DATABASE PROD TO ROLE AI_AGENT_READ;
GRANT USAGE ON SCHEMA PROD.MART TO ROLE AI_AGENT_READ;
GRANT SELECT ON ALL TABLES IN SCHEMA PROD.MART TO ROLE AI_AGENT_READ;
GRANT SELECT ON FUTURE TABLES IN SCHEMA PROD.MART TO ROLE AI_AGENT_READ;
CREATE USER AI_AGENT PASSWORD = '...' DEFAULT_ROLE = AI_AGENT_READ;
GRANT ROLE AI_AGENT_READ TO USER AI_AGENT;
Enforce row and time limits at the connector, not just in prompts:
# Python: add LIMIT, timeout, and block risky statements
MAX_ROWS = 5000
QUERY_TIMEOUT_S = 20
BLOCKLIST = ["UPDATE", "DELETE", "INSERT", "MERGE", "DROP", "ALTER"]
def enforce_limits(sql: str) -> str:
upper = sql.upper()
if any(word in upper for word in BLOCKLIST):
raise ValueError("Write-like SQL blocked")
if " LIMIT " not in upper:
sql = f"{sql.strip()} LIMIT {MAX_ROWS}"
return sql
def run_sql(sql):
with snowflake.connect(..., role="AI_AGENT_READ", warehouse="ANALYTICS_WH") as cxn:
cxn.cursor().execute(f"ALTER SESSION SET STATEMENT_TIMEOUT_IN_SECONDS={QUERY_TIMEOUT_S}")
return cxn.cursor().execute(sql).fetchmany(MAX_ROWS)
Test against your heaviest mart tables. For example, run the agent on a 40M-row orders table on an X-Small warehouse and log timeouts. Don’t guess costs—measure with your query history and warehouse credits to set sane defaults.
Grounding on dbt docs and a semantic layer
Text prompts aren’t a contract. Your agent should read your canonical metric definitions and model documentation. Expose dbt metadata (manifest + catalog) and, if you use the semantic layer, align queries to those metrics.
# dbt metric example (YAML)
models:
- name: fct_orders
description: Fact table of orders
columns:
- name: order_id
tests: [unique, not_null]
- name: order_total
description: Total value in USD
metrics:
- name: gross_revenue
model: ref('fct_orders')
type: sum
sql: order_total
timestamp: order_created_at
time_grains: [day, week, month]
filters:
- field: is_test_order
operator: '='
value: 'false'
# Python: load dbt docs for retrieval grounding
import json, pathlib
MANIFEST = json.loads(pathlib.Path('target/manifest.json').read_text())
CATALOG = json.loads(pathlib.Path('target/catalog.json').read_text())
def glossary():
terms = {}
for name, node in MANIFEST['nodes'].items():
if node.get('description'):
terms[node['name']] = node['description']
return terms
Feed the glossary and metric YAML to retrieval so the agent asks for gross_revenue by month instead of inventing a sum on a similarly named column. For teams using a central semantic layer, route through it to guarantee consistent aggregations and dimensions. This is where a little upfront plumbing prevents week‑two drift.
Tools the agent can call—and how to gate them
Keep tools explicit and typed. At minimum: query_sql, explain_sql, render_chart, and request_approval for anything that changes systems. Writes should always trigger human review.
# Tool schema (JSON Schema-like contract)
TOOLS = {
"query_sql": {
"description": "Run a read-only SQL against curated schemas",
"params": {
"type": "object",
"properties": {
"sql": {"type": "string"},
"purpose": {"type": "string"}
},
"required": ["sql"]
}
},
"request_approval": {
"description": "Ask a human to approve a side-effecting action",
"params": {
"type": "object",
"properties": {
"action": {"type": "string"},
"payload": {"type": "object"}
},
"required": ["action"]
}
}
}
# Human-in-the-loop prompt to the channel
def enqueue_human_review(channel, payload):
msg = (
"Proposed action requires approval.\n"
f"Action: {payload['action']}\n"
"React with ✅ to proceed, 🛑 to cancel."
)
post_message(channel, msg)
# a simple state machine waits for reactions then runs the integration
This pattern keeps your bot helpful while insulating production systems. For report attachments, render a chart server-side and post the image with a short summary. If you want deeper orchestration or routing across many tools, we build that as part of our internal AI platforms.
Evals and drift detection you can run weekly
Ship with a test pack. Store canonical question → expected SQL → expected shape → acceptable tolerance. Run it on a schedule and alert when behavior drifts. Keep golden answers small enough to compute quickly and update them when business logic changes.
# Minimal eval harness
import yaml, time
suite = yaml.safe_load(open('evals.yaml'))
failures = []
for case in suite['cases']:
sql = text_to_sql(case['question'], case.get('context', {}))
sql = enforce_limits(sql)
rows = run_sql(sql)
if not case['assert'](rows):
failures.append({"case": case['id'], "sql": sql, "rows": rows[:5]})
print({"passed": len(suite['cases']) - len(failures), "failed": len(failures)})
# evals.yaml (sketch)
cases:
- id: revenue_month
question: "What is gross revenue by month this year?"
assert: !!python/name:assert_revenue_month # implement in harness
Track latency, tool counts, and the fraction of answers that needed human approval. If you change the llm or prompt, run the suite first, then canary to a single team. Repeat the eval weekly; it catches the quiet regressions that appear as your schemas evolve.
What text-to-SQL still gets wrong (and how to contain it)
Even with grounding, you’ll see predictable misses:
- Joins on similarly named keys across marts, causing fan-out
- Time zones and week boundaries disagreeing with finance calendars
- Null handling and returned zero rows misread as “no data”
- Aggregations on pre-aggregated tables
- Filters that bypass your “is_test_order = false” convention
Mitigations that hold up in production:
- Route queries through models in curated schemas only
- Force GROUP BY on named time grains from your metrics YAML
- Reject queries that mix tables across domains without a documented relationship
- Attach an explain_sql step: the agent must show the final SQL before execution
- Always return the SQL and a short summary with the answer so humans can spot issues
# Guardrails in generation
ALLOWED_SCHEMAS = {"PROD.MART", "PROD.DIM"}
def validate_tables(sql):
tables = extract_tables(sql)
if not all(tbl.split('.')[0] + '.' + tbl.split('.')[1] in ALLOWED_SCHEMAS for tbl in tables):
raise ValueError("Unapproved schema in query")
Containment beats perfection. If the agent is uncertain, have it ask a clarifying question in the thread and prefer fewer, smaller queries over one giant guess.
Build vs. buy: custom agent or Agentforce and friends
There are credible off-the-shelf options. Salesforce is shipping Agentforce and a growing set of AI features. If your warehouse, Sales Cloud objects, and approvals live together, that can be a fast path. If your facts live in Snowflake/BigQuery and your team needs analytics answers inside Slack, a custom build keeps control over SQL and governance.
| Option | Strengths | Trade-offs |
|---|---|---|
| Custom agent in Slack | Exact metric semantics (dbt), read-only SQL, tailored tools | Own the plumbing, on-call, and evals |
| Agentforce | Tight with Salesforce objects, approvals, and Canvas | Warehouse joins may require extra integration work |
| Marketplace AI apps | Fast start, minimal setup | Limited control over schemas, fewer safety levers |
Questions to ask in your org: Will answers live inside Slack threads or in dashboards linked from Canvas? Do we need enterprise search across knowledge plus metrics? Which customer data must appear in channel without leaking PII? Will one slack agent serve more than one slack workspace? If you want a partner who has wired this to the flow of work, we document approaches on our AI agents articles page.
FAQ: short, practical answers
Can you suggest a few names for my Slack app?
Pick a noun that signals scope: Ledger, Atlas, Pulse, Beacon. Avoid “data” in the name; the icon and description can carry that.
Can Agentic AI in Slack resolve issues end-to-end without humans?
For reads and small automations, yes. For writes or cross-system changes, gate actions with an approval tool. That keeps risk low while still saving time.
Can I build custom agentic ai agents for Slack, and how?
Yes—use an event subscription, a small web service, and the tool pattern above. Ground on dbt and add evals before broad rollout.
Can’t get enough AI in the agent experience?
Add explain_sql, chart suggestions, and a one-line summary beside the full answer. Just keep prompts short and measure latency.
Do I need developer skills to build a Dust agent for Slack?
Non-developers can prototype, but production needs an engineer for OAuth, roles, and cost controls. A slack developer can take it the last mile.
Do Slack AI agents work for external customer support, or only internal teams?
They work for both, but external use raises auth and privacy needs. Keep PII masked and restrict channels.
Do Slack AI agents learn from past conversations?
They can store anonymized thread context or embed Q&A pairs. Retention should be short and configurable within slack.
Do Slack AI agents work across all my Slack workspaces?
Use an Enterprise Grid app if you need multi-org install. Keep data access separated per workspace and audit tokens.
Does AI in Slack work globally?
Yes, but watch data residency. Confirm where tokens and logs live, and align with your compliance posture.
Does Slack use customer data to train LLMs?
Review current policy from the vendor and your legal team. Many enterprise settings limit training on your slack data.
Does Slackbot work globally?
Yes, with network allowances. Latency varies; cache small lookups locally to keep the bot snappy.
Don’t have a paid plan?
You can still prototype with a free tier, but some APIs, retention, and security controls require paid.
Do we need a personal ai agent for work or a shared one?
Start with a shared channel agent. Later, consider a personal ai agent that knows an individual’s role and teams.
Where does an agent in slack fit with enterprise search?
Use search to retrieve docs and policies; use the agent for computation over curated data. They complement each other.
How do we keep the agent inside slack safe?
Limit scopes, use read-only roles, enforce limits, and route write actions through approvals in specific slack channels.
Which API endpoints matter most?
chat.postMessage, events for mentions, and file upload for charts are usually enough to start.
What is the right “ai assistant” shape?
A thread-native helper that posts the SQL it ran, a short summary, and links to the dashboard for deep dive.
When to ship, and when to call in help
If you’ve got OAuth, a read role, dbt docs, and a small eval pack, you can pilot to one team this week. If you want a hardened slack ai agent with cost guards, governance, and training for your analysts, we’ve done it many times—see our Slack AI agents service and broader internal AI platform work. We’ll meet you within slack, in your threads, and hand back a system your team can own.
Ready to move? Share your workspace constraints, warehouse, and target metrics, and we’ll scope a build-or-boost plan. Start a project with Vertex.
About the author
Eric Provencio — Analytics engineer who has built and run production data platforms for Disney, Hulu, Nike, Peloton, Gopuff, and Kaplan. Founded Vertex Data Consulting to do the deep work most data teams never find time for: dbt Cloud migrations, repo performance, Airflow reliability, and AI agents that actually touch the stack.