Saved queries hide OData syntax behind named, parametrised aliases. They let non-developer users (operations, finance, support) run their daily questions without remembering field names, operators, or escape rules — and without giving them a free-form CLI that could hit anything in the system.
# Look up a customer by name (no OData required)
bcli q customer-by-name name=Fabrikam
# List the saved queries available for the active profile
bcli q
# Inspect what a query resolves to without executing it
bcli q customer-by-name name=Fabrikam --showEach profile has its own queries file:
~/.config/bcli/queries/<profile>.yaml
If the file doesn't exist, bcli q prints a starter example and exits.
Create the file by hand, or hand-edit it with $EDITOR.
queries:
customer-by-name:
description: Look up a customer by display name
endpoint: customers
params:
name:
required: true
filter: "displayName eq '${{ params.name }}'"
select: "number,displayName,email,phoneNumber"
orderby: "displayName asc"
top: 25
open-invoices-by-customer:
description: Outstanding invoices for a customer
endpoint: customerSalesInvoices
params:
customer-id:
required: true
limit:
default: 50
filter: "customerNumber eq '${{ params.customer-id }}' and status eq 'Open'"
orderby: "dueDate asc"
top: "${{ params.limit }}"| Field | Type | Notes |
|---|---|---|
description |
string | Shown in bcli q listing and the generated slash command's frontmatter. |
categories |
list[str] | Optional. Used by bcli skill install to group commands in the generated SKILL.md index. Falls back to ["unsorted"]. |
args |
list[obj] | Optional. Explicit positional ordering for the generated slash command. If omitted, inferred from params: keys (required first, optional second). See Slash-command projection below. |
endpoint |
string | Required. Entity-set name (resolved through the registry). |
params |
mapping | Optional. Each key declares a parameter; see Param declarations below. |
filter |
string | OData $filter. Supports ${{ params.X }} substitution. |
select |
string | Comma-separated field list. |
expand |
string | Comma-separated navigation properties. |
orderby |
string | OData $orderby. |
top / skip |
int | Pagination bounds. |
all |
bool | If true, follows pagination to gather all records. |
Anything else in ${{ ... }} references the same template engine bcli batch
uses, so ${{ params.X }} works identically.
Each entry under params: is a small schema. The full set of keys:
| Key | Notes |
|---|---|
required |
If true, the user must pass key=value on the command line. |
default |
Value used when the user doesn't pass one. |
type |
One of string, integer, number, boolean. Drives coercion and per-type validation. Defaults to "no coercion" (the value is passed through as-is). |
pattern |
Regex (full-match) for string params. Rejected values fail locally before any HTTP call. |
min / max |
Numeric bounds for integer / number params. |
enum |
List of allowed literal values (any type). |
Example with all the validation knobs:
queries:
utilization-by-esn:
description: Monthly utilization records for one ESN
endpoint: engineUtilizations
params:
esn:
required: true
type: string
pattern: "^[A-Za-z0-9-]{4,16}$" # ESNs are alphanumeric, no spaces
limit:
default: 24
type: integer
min: 1
max: 1000
airline:
required: false
type: string
enum: ["AIRNORTH", "QANTAS", "VIRGIN"]
filter: "engineSerialNumber eq '${{ params.esn }}'"
orderby: "asOfDate desc"
top: "${{ params.limit }}"When a string-typed param is interpolated into the filter: field, bcli
applies OData v4 single-quote escaping (' → '') so a value like
193208' or 1 eq 1-- cannot break out of the surrounding string literal.
This escape is scoped to the filter context — select, orderby, top,
skip, all, and endpoint keep raw values, since they don't sit inside
OData string literals.
For defense in depth, declare a pattern: (or use type: integer) on params
that should not contain free-form text. The structural escape protects
against the syntactic case; the type / pattern check rejects malformed input
before it reaches the wire.
Note on
--showoutput. A vendor name likeO'Brienrendered with--showwill appear as'O''Brien'in the resolved filter — that's the OData literal form, not a bug. BC parses the doubled quote back to a single quote on the server side.
Saved queries pair well with bcli config init --scoped (see
docs/configuration.md). Together they give a non-developer user:
- a profile that can only see the endpoints they imported,
- a curated list of "questions they're allowed to ask",
- device-code login (no client secret to manage).
A typical setup for an operations team:
# 1. Admin creates the scoped profile and imports their endpoints
bcli config init --profile ops --scoped \
--category warehouse \
--import warehouse.postman_collection.json
# 2. Admin authors ~/.config/bcli/queries/ops.yaml with 5–10 daily questions
# 3. End user runs queries without touching OData
bcli --profile ops auth login # one-time browser sign-in
bcli --profile ops q # see what's available
bcli --profile ops q items-low-stock min=10--show— print the resolved request without executing it. Useful when reviewing what a saved query will actually send.--format— override the active profile's output format (json,markdown,csv,ndjson,table).--dry-run(global) — skips execution after resolving.
bcli skill install reads the saved queries for the active profile and
generates one Claude Code slash command per query at
<target>/.claude/commands/bcli-<name>.md, plus a top-level skill index
at <target>/.claude/skills/bcli/SKILL.md grouped by categories:.
Three optional fields on each query feed the generator:
queries:
utilization-by-esn:
description: Engine utilization (cycles, hours, FSN) for an ESN
categories: [aviation, daily-ops]
args:
- name: esn
type: string
example: "424322"
required: true
# existing fields below — params/filter/select/etc.
endpoint: util_history
params:
esn: {required: true}
filter: "engine_serial eq '${{ params.esn }}'"description— used as the slash command's frontmatterdescription:and listed under its category inSKILL.md.categories— list of strings. Each category becomes a section inSKILL.md. Queries with no categories land underunsorted.args— explicit positional ordering for the generated slash command body. Each entry:{name, type, required, example}. If omitted, the generator derivesargs:fromparams:keys (required first, optional withdefault:second, both in YAML insertion order). For most queries you can leaveargs:out and the projected command will still work; declare it explicitly when you want a different ordering than the params dict gives you.
The generated command body invokes bcli q <name> arg1=$1 arg2=$2 … --format json, so the positional → key mapping in the slash command
matches your args: order.
- Each generated file embeds a
content_hash: sha256:…line in its provenance comment. Re-runningbcli skill installon unchanged sources is a no-op (hash matches → file isn't rewritten). - To protect a hand-edited slash command file from regeneration, add
manual: trueto its YAML frontmatter:The installer skips any file whose frontmatter declares--- manual: true description: My customised command ---
manual: true, even if a saved query by the same name exists. - Add
--dry-runto preview without writing; add--target PATHto point at a specific project root (defaults to CWD when it contains a.claude/directory, else$HOME).
Batch workflow YAMLs under ~/.config/bcli/batches/<profile>/*.yaml are
projected the same way as saved queries — one
.claude/commands/bcli-batch-<name>.md per file, body invoking
bcli batch run <yaml> --set arg=$1 --format json --result-out ….
CWD-relative batch discovery (a batches/ directory checked into a
project repo) is a follow-up — open an issue if you need it.