Your first real investigation (dogfooding runbook)¶
Goal: get Gaby to investigate one real ticket against a real system with a real LLM, and watch the verdict appear. This is the end-to-end path the in-process investigation consumer runs; it is not the
gaby investigateCLI (that uses a fake tool dispatcher and won't touch your connectors).
About 15 minutes. You'll need Docker, an ANTHROPIC_API_KEY, and a
system to investigate against (this guide uses a Postgres database
e.g. a read replica of your app DB).
How the real path works¶
ticket (status="new") ──poller or manual insert──▶ tickets table
│
▼ InvestigationConsumerRuntime.claim_next() (claims status="new")
real agent loop
│ RealToolDispatcher ─▶ MCPHost ─▶ Postgres connector (read-only)
▼
verdict ─▶ /app/inbox + GET /api/investigations + hash-chained audit
Two switches must be on (both default in ops/docker/docker-compose.yml):
GABY_ENABLE_TICKET_POLLER=true, only needed if a real ticket source feeds ticketsGABY_ENABLE_INVESTIGATION_CONSUMER=true, the consumer that actually investigatesANTHROPIC_API_KEY=sk-..., without it the consumer logsinvestigation_consumer_no_provider_disabledand does nothing
1. Configure the stack¶
Edit ops/docker/.env:
ANTHROPIC_API_KEY=sk-ant-... # required (the consumer needs it)
GABY_ENABLE_INVESTIGATION_CONSUMER=true
GABY_ENABLE_TICKET_POLLER=true # ok to leave true; harmless with no sources
# If host port 8080 is taken (e.g. by another container):
# GABY_BACKEND_PORT=8081
2. Boot¶
docker compose -f ops/docker/docker-compose.yml --env-file ops/docker/.env up -d
docker logs -f gaby-gaby-backend-1 | grep -E "bootstrap|consumer|gaby_started"
You're looking for:
first_run_bootstrap_mintedwith a URL, open it to create the admininvestigation_consumer_started, the consumer is live (only logs ifANTHROPIC_API_KEYis set)
Open the bootstrap URL, create your admin account, then log in at the
web UI (http://localhost:8090).
3. Add a read-only Postgres connector¶
Point Gaby at the system the ticket will be about. Easiest via the UI (Connectors → Add → PostgreSQL), or by API:
# Grab your session cookie from the browser devtools, or log in via curl first.
curl -s http://localhost:8090/api/connectors \
-H 'Content-Type: application/json' \
-b "gaby_session=<your-cookie>" \
-d '{
"kind": "postgres",
"name": "App DB (read replica)",
"config": { "DATABASE_URL": "postgresql://readonly:pw@db.internal:5432/app" },
"autonomy_level": "investigate"
}'
investigate autonomy keeps it read-only, the connector can SELECT,
never write. Verify it started:
curl -s http://localhost:8090/api/connectors -b "gaby_session=<cookie>" | jq '.[].status'
# expect "ready"
Use a real, scoped credential
Give Gaby a read-only Postgres role. The connector is read-only by design, but defense in depth means the credential should be too.
4. Get a ticket into the queue¶
The consumer claims tickets with status="new". Two ways:
Configure a help desk in Onboarding → step 2 (Zoho Desk, email/IMAP,
or Slack). The poller pulls new tickets and queues them as status="new".
Send yourself a test ticket through that channel.
Insert one row directly. For the default SQLite store inside the container:
docker exec -it gaby-gaby-backend-1 python - <<'PY'
import asyncio, uuid
from datetime import UTC, datetime
from gaby.config import Settings
from gaby.storage.db import make_engine, make_session_factory
from gaby.storage.models.ticket import Ticket
async def main():
s = Settings()
sf = make_session_factory(make_engine(s))
async with sf() as session, session.begin():
session.add(Ticket(
id=str(uuid.uuid4()),
workspace_id=s.default_workspace_id,
external_id="dogfood-1",
title="Login returns 500 for one customer",
body="Customer acme-co reports a 500 on login since this morning. "
"Other customers are fine. Can you find the cause?",
customer="acme-co",
status="new",
received_at=datetime.now(UTC),
))
print("queued ticket dogfood-1")
asyncio.run(main())
PY
Write the ticket body so the answer lives in the system you connected e.g. a row state in Postgres the agent can find by querying.
5. Watch it investigate¶
docker logs -f gaby-gaby-backend-1 | grep -E "claimed|tool.executed|investigation_worker_completed|verdict"
You should see the consumer claim the ticket, the loop call your Postgres
connector (tool.executed), and a verdict. Then inspect it:
Or open Inbox in the web UI, the ticket shows up live (SSE), and the investigation detail shows the step-by-step trajectory: which tools were called, what they returned, and the final verdict + drafted reply.
At investigate autonomy the reply is drafted, not sent, read-only,
nothing is written back to the help desk. That's the safe default.
6. Verify the audit trail¶
Every tool call and decision is in the hash-chained audit log:
This is the artifact a security-conscious buyer wants to see: a tamper- evident record of exactly what the agent read and decided.
What "good" looks like¶
- The agent calls the Postgres connector with a sensible query (not a guess)
- The verdict cites what it found
gaby audit verifypasses- Zero writes to your systems (it's read-only at
investigate)
Capture the numbers¶
For the dogfooding metrics that feed the GTM story, track across a batch of real tickets:
- resolve-or-correctly-escalate rate (the bar to beat: ~67%, Intercom Fin's published average)
- time-to-verdict
- escalation accuracy (did it escalate the ones a human would have?)
- cost per investigation (Settings → Costs)
Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
investigation_consumer_no_provider_disabled in logs |
No ANTHROPIC_API_KEY |
Set it in .env, recreate the backend container |
Ticket sits in new, never claimed |
Consumer not enabled | GABY_ENABLE_INVESTIGATION_CONSUMER=true, restart |
Connector status degraded |
Bad DATABASE_URL / unreachable DB |
Fix the connection string; check the container can reach the DB host |
port is already allocated on boot |
Host 8080 taken | Set GABY_BACKEND_PORT=8081 in .env |
| Verdict ignores the system data | Connector not ready, or ticket body doesn't point at queryable data |
Confirm connector ready; write the ticket so the answer is findable |