# Datasets API and CLI
Datasets are exposed through two ConnectRPC services:
- `bountyhub.dataset.v1.DatasetService` — datasets, ingest, SQL, describe, export.
- `bountyhub.dataset.v1.DatasetViewService` — saved views.
The generated TypeScript SDK exposes `datasetClient` and `datasetViewClient`; the
`bh` CLI wraps the most common `DatasetService` calls. In Connect JSON, `int64`
and `uint64` fields (for example `rowCount`) are encoded as strings.
## Authentication and scopes
|
Access
|
Scope
|
|
Get, list, query, project-wide query, describe, export, execute view
|
DatasetRo
|
|
Create, update, delete, ingest, delete rows, clear, create/update/delete view
|
DatasetRw
|
Create a Personal Access Token with the **Datasets** scope (and **Projects**
read, which it requires). See
[Personal Access Token scopes](/developer/scopes).
## DatasetService
|
RPC
|
Purpose
|
Pagination
|
CreateDataset
|
Create a table and schema
|
—
|
GetDataset
|
Read one dataset
|
—
|
ListDatasets
|
List project datasets
|
Default 50, max 500
|
UpdateDataset
|
Change name or description
|
—
|
DeleteDataset
|
Delete the dataset and its files
|
—
|
IngestDatasetRows
|
Append observations
|
—
|
DeleteDatasetRows
|
Soft- or hard-delete records selected by a filter
|
—
|
ClearDataset
|
Remove every record and stored file
|
—
|
QueryDataset
|
One read-only SELECT over
dataset
|
Default 100, max 1,000
|
QueryDatasets
|
One SELECT over every dataset in a project
|
Default 100, max 1,000
|
DescribeDataset
|
Null/distinct counts and top values
|
—
|
ExportDataset
|
Stream a full query result to a file
|
—
|
### CreateDataset
```json
{
"projectId": "",
"name": "subdomains",
"description": "Resolved subdomains per program",
"schema": {
"columns": [
{ "name": "host", "kind": "COLUMN_KIND_TEXT", "nullable": false, "identity": true },
{ "name": "program", "kind": "COLUMN_KIND_TEXT", "nullable": false, "identity": true },
{ "name": "resolved", "kind": "COLUMN_KIND_BOOLEAN", "nullable": true }
]
}
}
```
Returns `{ "id": "" }`. Names must be unique per project, sharing
one case-insensitive namespace with saved views; up to 100 datasets are allowed.
See [Schemas and identity](/datasets/schema).
### IngestDatasetRows
```json
{
"datasetId": "",
"mode": "DATASET_INGEST_MODE_UPSERT",
"rows": [
{
"fields": {
"host": { "textValue": "api.example.com" },
"program": { "textValue": "example" },
"resolved": { "booleanValue": true }
}
}
]
}
```
- `fields` is keyed by column name; the service maps values by schema order.
- Missing fields become `NULL`; a `NULL` in a non-nullable column fails.
- Values must match the column kind (see
[Structured filters](/datasets/filters#value-types) for the value shape).
- `mode` is `DATASET_INGEST_MODE_UPSERT` (default) or
`DATASET_INGEST_MODE_SYNC`.
- Returns `{ "rowCount": "" }`.
### DeleteDatasetRows
```json
{
"datasetId": "",
"mode": "DATASET_DELETE_MODE_SOFT",
"filter": {
"condition": {
"column": "host",
"op": "FILTER_OP_EQ",
"values": [{ "textValue": "api.example.com" }]
}
}
}
```
`filter` is required and selects records from the current logical state.
`DATASET_DELETE_MODE_SOFT` appends tombstones and requires identity columns;
`DATASET_DELETE_MODE_HARD` removes the matching observations from stored files.
Returns `{ "deletedRowCount": "", "storageDeltaBytes": "" }`.
### ClearDataset
```json
{ "datasetId": "" }
```
Removes every record and stored file. Returns `{ "deletedRowCount": "", "storageDeltaBytes": "" }`.
### QueryDataset
```json
{
"datasetId": "",
"sql": "SELECT host, ip FROM dataset WHERE resolved",
"pageSize": 100,
"pageToken": "",
"includeHistory": false,
"ingestedAtFrom": "2026-01-01T00:00:00Z",
"filter": {
"condition": {
"column": "ip",
"op": "FILTER_OP_IS_NOT_NULL",
"values": []
}
}
}
```
|
Field
|
Meaning
|
sql
|
One read-only SELECT; a bare
SELECT *
becomes
SELECT * FROM dataset
.
|
pageSize
|
Default 100, max 1,000.
|
pageToken
|
Cursor from
nextPageToken
.
|
includeHistory
|
Query raw observations instead of current state.
|
ingestedAtFrom
/
ingestedAtTo
|
Bound
__ingested_at
.
|
filter
|
Typed filter tree; see
Structured filters
.
|
The response contains `schema.columns`, `rows` (`values` in schema order), and
`nextPageToken`.
### QueryDatasets
```json
{
"projectId": "",
"sql": "SELECT s.host, w.status_code FROM subdomains AS s JOIN \"web-services\" AS w ON s.host = w.host WHERE s.resolved",
"pageSize": 100
}
```
Returns `{ "result": { "columns", "rows", "nextPageToken" } }`. Filter with the
SQL `WHERE` clause; the per-relation `filters` field is reserved and not
supported by the current execution path. This path has no history mode.
Project-wide SQL is API-only; the Hub UI's SQL box is scoped to one dataset.
### DescribeDataset
```json
{ "datasetId": "" }
```
Returns one entry per schema column with:
|
Field
|
Meaning
|
nullCount
|
Rows where the column is
NULL
.
|
distinctCount
|
Distinct non-null values.
|
topValues
|
Up to 10 most frequent non-null values, most frequent first.
|
`bh dataset describe` prints the first three top values.
### ExportDataset
```json
{
"datasetId": "",
"format": "DATASET_EXPORT_FORMAT_CSV",
"sql": "SELECT * FROM dataset",
"includeHistory": false
}
```
|
Format
|
Enum
|
Content type
|
|
CSV
|
DATASET_EXPORT_FORMAT_CSV
|
text/csv
|
|
JSON Lines
|
DATASET_EXPORT_FORMAT_JSONL
|
application/x-ndjson
|
|
JSON array
|
DATASET_EXPORT_FORMAT_JSON
|
application/json
|
- `sql` is required; use `SELECT * FROM dataset` for a full export.
- Filters and time bounds use the same outer semantics as `QueryDataset`.
- Exports have no page size or row cap; rows stream to the file.
- The response returns `url` (presigned, valid for **15 minutes**), `fileName`,
`contentType`, `rowCount`, and `sizeBytes`.
## DatasetViewService
|
RPC
|
Scope
|
Notes
|
CreateDatasetView
|
DatasetRw
|
Validates SQL, references, and cycles.
|
GetDatasetView
|
DatasetRo
|
Returns
valid
and
invalidReason
.
|
ListDatasetViews
|
DatasetRo
|
Default 50, max 500.
|
UpdateDatasetView
|
DatasetRw
|
Changes name, description, and SQL; resets validity.
|
DeleteDatasetView
|
DatasetRw
|
Invalidates dependent views.
|
ExecuteDatasetView
|
DatasetRo
|
Default 100, max 10,000.
|
`ExecuteDatasetView` returns `{ "result": { "columns", "rows", "nextPageToken" } }`. Views run over the current state and have no filter,
history, or time-range fields; see [Saved views](/datasets/views).
## bh CLI
The CLI wraps `DatasetService` for the common read and manage paths:
```bash
# List datasets in a project
bh dataset list --project-id "$PROJECT_ID" --all
# Inspect one dataset and its columns
bh dataset get --dataset-id "$DATASET_ID"
bh dataset describe --dataset-id "$DATASET_ID"
# Create with a schema file (proto JSON)
bh dataset create \
--project-id "$PROJECT_ID" \
subdomains \
--description "Resolved subdomains per program" \
--schema-json schema.json
# Rename or change the description
bh dataset update \
--dataset-id "$DATASET_ID" \
--name subdomains-2026 \
--description "Subdomains discovered in 2026"
# Query current state or history
bh dataset query \
--dataset-id "$DATASET_ID" \
"SELECT host, ip FROM dataset WHERE resolved"
bh dataset query \
--dataset-id "$DATASET_ID" \
--include-history \
"SELECT host, \"__ingested_at\" FROM dataset"
# Delete
bh dataset delete --dataset-id "$DATASET_ID"
```
`bh dataset query` prints a `next page token` line when more rows are available
and accepts `--page-size` (default 100). It has no page-token flag; use the API
(`QueryDataset`) to continue past the first page.
There are no CLI commands for project-wide SQL, saved views, exports, or row
ingestion — use the API for those.
## Errors
|
Connect code
|
Cause
|
UNAUTHENTICATED
|
Missing or invalid token.
|
PERMISSION_DENIED
|
The PAT lacks
DatasetRo
/
DatasetRw
.
|
NOT_FOUND
|
Unknown dataset or view, or one outside the caller's projects.
|
ALREADY_EXISTS
|
Dataset name already used in the project.
|
INVALID_ARGUMENT
|
Invalid schema, SQL, filter, id, or page token; a view cannot be evaluated.
|
RESOURCE_EXHAUSTED
|
Too many concurrent queries or too many per minute.
|
## Related
- [Schemas and identity](/datasets/schema) — column kinds and identity.
- [SQL reference](/datasets/sql) — query language and limits.
- [Structured filters](/datasets/filters) — filter AST and value types.
- [Saved views](/datasets/views) — view semantics.
- [Personal Access Token scopes](/developer/scopes) — scope names.