Reading resources
Read requests across SPEKTRA Edge share the same four controls, whichever service and whichever protocol you use:
| Control | Question it answers |
|---|---|
filter |
Which resources? |
view and fieldMask |
Which fields of each? |
orderBy |
In what order? |
pageSize and pageToken |
How many at a time? |
Learn them once and every collection in the platform is queryable. Getting them right is also the difference between an integration that stays fast at ten thousand devices and one that does not.
Filtering
A filter is a SQL-flavored expression evaluated server-side.
status.connectionStatus="CONNECTED" AND metadata.labels CONTAINS "site:berlin"
Field paths are the resource’s fields in lowerCamelCase, dotted to reach
nested messages, exactly as they appear in the JSON representation. String
values must be double-quoted. Keywords are case-insensitive, so AND and
and are equivalent.
AND is also implicit — adjacent conditions are combined without it, and +
works as an alias. These three are the same filter:
spec.enabled=true AND status.state="READY"
spec.enabled=true status.state="READY"
spec.enabled=true + status.state="READY"
You can annotate long filters: -- begins a comment that runs to the end of
the line, and newlines are insignificant.
-- tills that have stopped reporting
status.connectionStatus="DISCONNECTED"
metadata.labels CONTAINS "role:till"
Operators
These are implemented and safe to rely on:
| Operator | Example |
|---|---|
= != < > <= >= |
spec.osVersion!="1.0.2" |
IN |
status.state IN ["READY","DEGRADED"] |
CONTAINS (alias HAS) |
metadata.labels CONTAINS "site:berlin" |
CONTAINS ANY |
metadata.labels CONTAINS ANY ["site:berlin","site:hamburg"] |
CONTAINS ALL |
metadata.labels CONTAINS ALL ["role:till","env:prod"] |
IS NULL, IS NOT NULL |
status.lastSeenTime IS NOT NULL |
AND (implicit, or +) |
see above |
List arguments accept either brackets or parentheses — IN ["a","b"] and
IN ("a","b") are the same.
The filter grammar is broader than the query engine behind it. OR (and its
| alias), NOT, NOT IN, LIKE, and IS NAN are all accepted by the
parser and then rejected when the query executes, as an UNIMPLEMENTED error.
The generated API reference and the cuttle --filter help text describe the
filter grammar, not what the query engine executes, so both list LIKE
and OR. The operator table above is the list that executes.
A filter is therefore not validated by “it parsed”. Compose with AND only,
and see Unions below for what to do when you genuinely need OR.
There is one further restriction. When comparing a name or reference field
against a value containing a - wildcard, only = is available; anything else
is rejected.
Values
| Type | Form |
|---|---|
| String | "berlin" — always double-quoted |
| Number | 42, -1.5, 1e3 |
| Boolean | true, false |
| Null | NULL |
| Map | {"key":"value"} |
Timestamps are strings in RFC 3339:
metadata.createTime>"2026-01-01T00:00:00Z"
Labels
Labels are a map, and the whole map is one field path. Match an entry with
CONTAINS and a key:value string, not by indexing into it:
metadata.labels CONTAINS "site:berlin"
This is why labels are the practical way to segment a fleet: one indexed field supports the queries, the alert groupings, and the deployment targeting.
If a label key itself contains a dot, escape it with a backslash so the parser does not read it as a path separator.
Unions and negation
Because OR and NOT do not execute, express them client-side:
- Union — run one query per branch and merge the results, de-duplicating
on
name. Often a singleINorCONTAINS ANYcollapses the branches anyway, which is cheaper; check for that first. - Negation — invert the operator where you can.
NOT (state="READY")becomesstate!="READY";NOT IN ["a","b"]usually has to become a client-side filter over a broader query.
Prefer collapsing to IN/CONTAINS ANY over multiple round trips whenever the
branches differ only in a value.
Choosing which fields you get
By default a read returns a standard set of fields. Two controls change that, and they combine; they are not alternatives.
view picks a baseline:
| View | Returns |
|---|---|
NAME |
Only the resource name. |
BASIC |
A commonly useful subset. The usual default. |
DETAIL |
More, including heavier fields. |
FULL |
Everything. |
fieldMask is additive on top of the view — a list of field paths to
include as well. This trips people up: setting a field mask does not restrict
you to those fields, it adds them to whatever the view already returns. For a
minimal response, combine view=NAME with a field mask of exactly what you
need:
curl -s -H "Authorization: Bearer $API_KEY" --get \
--data-urlencode 'view=NAME' \
--data-urlencode 'fieldMask=status.connectionStatus,metadata.labels' \
"https://devices.apis.edgelq.com/v1/projects/$PROJECT/regions/$REGION/devices"
The saving is real. Device resources carry substantial hardware inventory and status; asking for two fields instead of all of them changes both response size and latency materially across a large fleet.
Ordering
orderBy takes field paths with an optional direction, asc (the default) or
desc:
orderBy=metadata.createTime desc
Ordering must be stable for pagination to be correct, so prefer a field that is unique or append a tie-breaker.
Pagination
Set pageSize, then follow the tokens the server returns. Never assume the
server honoured your page size — it may return fewer, and a short page does
not mean the last page. The only reliable end-of-results signal is an
empty nextPageToken.
TOKEN=""
while : ; do
RESP=$(curl -s -H "Authorization: Bearer $API_KEY" --get \
--data-urlencode "pageSize=100" \
--data-urlencode "pageToken=$TOKEN" \
"https://devices.apis.edgelq.com/v1/projects/$PROJECT/regions/$REGION/devices")
echo "$RESP" | jq -r '.devices[]?.name'
TOKEN=$(echo "$RESP" | jq -r '.nextPageToken // empty')
[ -z "$TOKEN" ] && break
done
Responses also carry prevPageToken, so a cursor can be walked in either
direction.
If you need to render “page 3 of 47”, set includePagingInfo=true and the
response adds currentOffset and totalResultsCount. It costs the server extra
work to count, so leave it off when you are just iterating.
Keep pageSize modest — a few hundred at most. Large pages increase the chance
of a timeout on a slow query, and a retry then repeats the whole page.
Search
Some resources also expose Search, which takes a free-text phrase in
addition to the controls above:
GET /v1/projects/{project}/regions/{region}/devices:search?phrase=till
Use Search for a human typing into a box, and List with a filter for
anything programmatic. Search trades exactness for recall, which is the wrong
trade for automation.
Reading across regions
Substitute - for the region to query every region a project uses:
GET /v1/projects/{project}/regions/-/devices
BatchGet is the other cross-region read — it takes names instead of a
parent, so one call can fetch resources from several regions at once.
Nested request fields over REST
The four controls above are flat, but some reads take structured request fields. Over gRPC you set them as nested messages; over REST, each leaf becomes a dotted query parameter. Reading time series is the case you are most likely to hit:
curl -s -H "Authorization: Bearer $API_KEY" --get \
--data-urlencode 'filter=metric.type="devices.edgelq.com/device/cpu/usage"' \
--data-urlencode 'interval.startTime=2026-09-01T00:00:00Z' \
--data-urlencode 'interval.endTime=2026-09-02T00:00:00Z' \
--data-urlencode 'aggregation.alignmentPeriod=60s' \
--data-urlencode 'aggregation.perSeriesAligner=ALIGN_MEAN' \
--data-urlencode 'aggregation.crossSeriesReducer=REDUCE_NONE' \
"https://monitoring.apis.edgelq.com/v4/projects/$PROJECT/timeSeries"
Three things to note. The field names are the JSON ones, so alignment_period
in the .proto is aggregation.alignmentPeriod here. interval.endTime is
required while interval.startTime is optional and defaults to the end time,
so omitting the start gives you an empty window, not everything. And
durations are strings with a unit suffix, such as 60s.
For what the aligners and reducers actually compute, see the Monitoring API reference. For the metric types you can filter on, see the device metrics reference.
Trying it interactively
Every control here has a cuttle flag, which makes the CLI the fastest way to
iterate on a filter before committing it to code:
cuttle devices list devices \
--parent projects/$PROJECT/regions/- \
--filter 'metadata.labels CONTAINS "site:berlin"' \
--field-mask status.connectionStatus \
--order-by 'metadata.createTime desc' \
--page-size 50 \
-o json
See Operating with the cuttle CLI for the CLI-side detail.
Next steps
- Watching for changes — replace polling with a live stream.
- Errors and limits — what a rejected filter looks like.