Skip to content

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 investigate CLI (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 tickets
  • GABY_ENABLE_INVESTIGATION_CONSUMER=true, the consumer that actually investigates
  • ANTHROPIC_API_KEY=sk-..., without it the consumer logs investigation_consumer_no_provider_disabled and does nothing

1. Configure the stack

cp ops/docker/.env.example ops/docker/.env

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_minted with a URL, open it to create the admin
  • investigation_consumer_started, the consumer is live (only logs if ANTHROPIC_API_KEY is 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:

curl -s http://localhost:8090/api/investigations -b "gaby_session=<cookie>" | jq '.[0]'

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:

docker exec -it gaby-gaby-backend-1 gaby audit verify

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 verify passes
  • 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