# Query

> whisper query runs read-only Cypher against WhisperGraph from the command line, with --param for safe parameters and --json for the columns, rows and statistics envelope.

*Source: https://www.whisper.security/docs/cli/query*

---
`whisper query` sends one Cypher statement to WhisperGraph and prints the answer. It goes to the same endpoint the [HTTP API](/docs/cypher-api) documents and gets the same reply back. The CLI only saves the typing.

[Sign in](/docs/cli#sign-in) first. Then:

```bash
whisper query "CALL whisper.identify(['api.openai.com'])"
```

```text
host            vendor_id   canonical_name  is_canonical  confidence  category  roles                band     host_class
api.openai.com  cloudflare  Cloudflare      true          0.8         cdn       ["ORIGIN_AS","CDN"]  DERIVED  unknown
1 row(s)
```

Anything the [Cypher](/docs/cypher) chapter describes runs here: `MATCH` traversals, the [procedures](/docs/whisper-graph/procedures), `CALL db.schema()`. Writes are refused by the graph before they run, so there is nothing to be careful about.

## Parameters

Never paste an indicator into the query string. Bind it with `--param` and refer to it as `$name`:

```bash
whisper query 'CALL whisper.assess([$v])' --param v=8.8.8.8
```

The value is parsed as JSON when it parses (numbers, booleans, lists, objects) and taken as a string otherwise. `8.8.8.8` is not valid JSON, so it arrives as the string the procedure expects. A list works the same way:

```bash
whisper query 'CALL whisper.assess($hosts)' --param 'hosts=["example.com","8.8.8.8"]'
```

```text
host         label               band  coverage
example.com  benign-allowlisted  NONE  known-clean
8.8.8.8      benign-allowlisted  INFO  known-clean
2 row(s)
```

`--param` repeats, one per parameter.

## JSON output

`--json` prints the reply exactly as the graph sent it: `columns`, `rows` and `statistics`.

```bash
whisper query 'CALL whisper.assess([$v])' --param v=8.8.8.8 --json
```

```json
{
  "columns": ["host", "label", "band", "sub_labels", "signals", "coverage", "evidence", "verdictScore", "isThreat", "threatSources"],
  "rows": [
    {
      "host": "8.8.8.8",
      "label": "benign-allowlisted",
      "band": "INFO",
      "coverage": "known-clean",
      "verdictScore": 0.0,
      "isThreat": false
    }
  ],
  "statistics": {"rowCount": 1, "executionTimeMs": 0}
}
```

That is the envelope from [POST /api/query](/docs/cypher-api/reference/query-post), so anything you already pipe API output into reads this too:

```bash
whisper query 'CALL whisper.assess([$v])' --param v=8.8.8.8 --json | jq -r '.rows[0].band'
```

Read `coverage` before `band`. `INFO` on a known-clean address and `NONE` on an address nobody has ever looked at print the same colour in a terminal, and only one of them is reassuring. [Coverage](/docs/whisper-graph/coverage) explains the difference.

## When something goes wrong

Errors print as one line beginning `whisper:` and the command exits with status 1. A write is refused the same way:

```bash
whisper query 'CREATE (n:X) RETURN n'
```

```text
whisper: read_only
```

No key, a syntax error and an unreachable graph all arrive in that shape, so a script can test the exit code and read the line. Zero rows is not an error: it means the query matched nothing, and the [Best Practices](/docs/cypher/best-practices) page has the usual reasons why.

## Where next

- [Recipes from the terminal](/docs/cli/recipes): the common questions, already written, one name each.
- [Cheat Sheet](/docs/cypher/cheat-sheet): edge directions, procedure columns and the query rules on one page.
