Query and Filter DSL Specification¶
9. Query and filter DSL¶
First-class query parameters¶
| Parameter | Meaning |
|---|---|
type_id |
Type id. Comma-separated = matches ANY (type_id=frontend-task,programming-task → IN); a single value is exact equality. |
status |
Column id. Comma-separated = matches ANY (status=todo,in_progress → IN); a single value is exact equality. |
owner |
User id (exact). The me alias is resolved by the board UI to the viewing actor, not by this API param — ?owner=me on GET /cards matches the literal owner me. |
tag |
Tag(s) |
q |
Full-text search (FTS5) |
has_link |
Link type id present |
link_target |
Card id linked |
blocked |
Shorthand: outgoing blocked-by/depends-on to a non-done card |
board_id |
Apply board default_filter + type/column scope |
Pagination: limit (default 50, max 500), cursor (opaque; keyed to the
default updated_at, id order).
Ordering is orthogonal to filtering: filters select which cards; sort
selects their order. sort takes one key (created_at, updated_at,
title, or fields.<id>) with an optional leading - for descending;
missing-field cards sort last; an unknown key is a 422. sort cannot be
combined with cursor (the keyset cursor is welded to the default order) — a
custom sort returns no next_cursor.
Note:
updated_before/updated_after/created_before/created_afterare not implemented as separate query params onGET /cards. JSON filter DSL is implemented for boarddefault_filterandtake-next/ CLI--filter-file, but not as afilter=query parameter onGET /cards.
Filter JSON (board defaults and take-next)¶
jq-like, compiled to SQL safely (not full jq). The DSL is used by board
default_filter and by take-next / cards take-next --filter-file; GET
/cards?filter= is not currently wired:
{
"$and": [
{ "owner": { "$eq": "me" } },
{ "status": { "$nin": ["done", "cancelled"] } },
{ "fields.priority": { "$eq": "high" } }
]
}
$eq, $ne, $in, $nin, $gt, $gte, $lt, $lte,
$contains, $has, $and, $or. Paths: fields.<id> for typed fields;
top-level keys for status, owner, type_id, tag, updated_at. CLI:
cards take-next --filter-file q.json. Power users: cards export --format
jsonl and local jq out of band.
$containssemantics: on a string-valued path it is a case-insensitive substring match (SQLiteLIKE); on an array-valued path (e.g.tags) it is an exact membership test (case-sensitive=).$eq/$instring comparisons are case-sensitive (=).
$hassemantics (multi-value fields) [built]: exact membership over afields.<id>path —{"fields.platforms": {"$has": "mobile"}}matches cards whose array contains the value (case-sensitive=, viajson_each). On a scalar-valued field it degrades to equality; an absent key never matches. Valid only onfields.<id>paths (core columns are never arrays —$hasthere is a loud error). This is the v1 filtering story formultiple: truefields:$eqon an array compares the whole JSON blob and is almost never what you want.
Recipes¶
- Cards in either lane:
status=todo,in_progress(comma = IN; scoped to theGET /v1/cardsfirst-class params above — the only endpoint that splits these lists. The board UI resolves anowner=mechip to the viewing actor before calling the API;?owner=meon the API itself matches the literal owner). - Blocked stale for take-next: request body
blocked=true+filter={"updated_at":{"$lt":"<now-1h>"}}.
10. Validation and anti-hallucination¶
Rules:
- Unknown enum value →
unknown_enum, echovalid_options. - Unknown tag (
tag_policy: locked) →unknown_tag, echotag_set; the hint names the configured policy. Not raised undertag_policy: open. - Unknown user →
unknown_user, include registration call. - Unknown field (strict mode) →
unknown_field, echo field list. - card_link to missing card →
target_card_missing, echo target type + search hint. - Link type/source/target mismatch →
target_card_type_mismatch, echo validsource_types/target_types. - Missing required field →
validation_failed, list missing fields. - Repeating entry missing required sub-field → per-entry rejection with
entry_id/index. - Schema version mismatch on write →
schema_version_mismatch, echocurrent_schema_version+ upgrade hint. - Optimistic concurrency: stale
version→version_conflict(409) with current card. - Illegal transition (enforced) →
transition_illegal, echo allowed next statuses. - No actor on a write →
actor_required(403).
dry_run: true validates fully and returns the would-be card + warnings,
writing nothing. A successful dry_run response returns the would-be card
(or would-be result) with a Dry-Run: true response header; the response
body is not otherwise marked as a dry run. Errors are structured JSON:
{
"error": "unknown_enum",
"field": "status",
"value": "In-Progress",
"message": "Unknown status. Use a board column id.",
"valid_options": ["todo", "in_progress", "review", "done"],
"hint": "See GET /workspace"
}
Error catalog¶
error |
HTTP | Carries |
|---|---|---|
validation_failed |
422 | field[], message |
unknown_enum |
422 | field, value, valid_options |
unknown_tag |
422 | value, valid_options (tag_set) |
unknown_user |
422 | value, hint |
unknown_field |
422 | field, valid_options |
target_card_missing |
422 | value, target_type, hint |
target_card_type_mismatch |
422 | value, valid_options |
transition_illegal |
422 | from, valid_options |
schema_version_mismatch |
422 | current_schema_version, hint |
version_conflict |
409 | current card |
actor_required |
403 | hint |
not_found |
404 | resource |
A replayed mutation (same Idempotency-Key) returns the original response
body and status with an added Idempotent-Replay: true response header —
not a distinct error code.