gastownhall/beads · error

invalid query expression: %s

Error message

invalid query expression: %s

What it means

invalidQueryExpression is the single refusal shape for any expression that fails — an empty expression, or one query.Parse cannot parse — always formatted as 'invalid query expression: <detail>' and wrapping issueops.ErrValidation. The prefix is load-bearing: internal/httpapi maps it back to the `q` parameter so a bad expression surfaces as the documented 400 rather than a 500.

Source

Thrown at internal/workapi/query.go:131

		// backend, and the probe row is genuinely last.
		plan.Filter.Limit = limit
		plan.Filter.SortBy = in.SortBy
		plan.Filter.SortDesc = in.Reverse
	}

	// The default closed exclusion, applied only to an expression that has no
	// opinion of its own about status (issueops/querier.go:29-41).
	if !in.IncludeClosed && plan.Filter.Status == nil && !mentionsStatus(node) {
		plan.Filter.ExcludeStatus = append(plan.Filter.ExcludeStatus, types.StatusClosed)
	}
	return plan, nil
}

// invalidQueryExpression is the one refusal shape a bad expression takes. The
// prefix is load-bearing: internal/httpapi maps it back to the `q` parameter
// so an unparseable expression is the documented 400 rather than a 500.
func invalidQueryExpression(detail string) error {
	return fmt.Errorf("invalid query expression: %s%.0w", detail, issueops.ErrValidation)
}

// mentionsStatus reports whether the expression compares `status` anywhere.
// An expression that does keeps its own answer about closed rows; one that
// does not gets the default exclusion.
func mentionsStatus(node query.Node) bool {
	switch n := node.(type) {
	case *query.ComparisonNode:
		return n.Field == "status"
	case *query.AndNode:
		return mentionsStatus(n.Left) || mentionsStatus(n.Right)
	case *query.OrNode:
		return mentionsStatus(n.Left) || mentionsStatus(n.Right)
	case *query.NotNode:
		return mentionsStatus(n.Operand)
	default:
		return false
	}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Read the detail after the prefix — it names the parse failure or 'an expression is required'
  2. Check errors.Is(err, issueops.ErrValidation) to confirm it is a client-input 400, not a server fault
  3. Validate the expression syntax against the query language docs (comparison operators, quoted values)
  4. Guard against empty/whitespace input before calling: strings.TrimSpace(expr) != ""

Example fix

// before
bd query 'status = '
// after
bd query 'status = open'
Defensive patterns

Strategy: try-catch

Validate before calling

expr := strings.TrimSpace(userExpr)
if expr == "" {
	return errors.New("query expression is required")
}

Try / catch

plan, err := BuildQueryPlan(req)
var verr error
if err != nil {
	if errors.Is(err, issueops.ErrValidation) && strings.HasPrefix(err.Error(), "invalid query expression") {
		// documented 400 path: map detail back to the q parameter
	}
	return err
}

Prevention

When it happens

Trigger: Calling BuildQueryPlan with in.Expression empty after trimming, or containing syntax query.Parse rejects — unbalanced quotes/parens, unknown operators, malformed comparisons like `status =`.

Common situations: Users typing ad-hoc `bd query 'status == open'` with wrong operator syntax; templates interpolating an empty variable into the expression; quoting issues in shells stripping inner quotes; schema changes renaming a field an old saved expression references.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/3369fb9e6b857730. Report an issue: GitHub.