
I got paged in the middle of the night this week by a little worker script I run against my Plane board. The alert itself turned out to be nothing — a provider hiccup, no data harmed. But since I was awake and grumpy anyway, I did what any paranoid sysadmin does: I audited whether the script had half-processed anything while it was flailing.
Everything was fine. No orphaned items, no half-mutated states. Lucky.
But that audit is how I noticed something that genuinely bothered me: the API call my script uses to ask "give me the work items in this specific state" had been returning... everything. Every state. Done items, cancelled items, the whole board — with a cheerful 200 OK on top.
No error. No warning. Just silently wrong.
The setup
Quick context. I manage my side projects in Plane — the open-source Jira/Linear alternative. I've got a small Python worker that polls the board every few hours through its REST API, picks up whatever needs doing, and moves items through the workflow states (Backlog → Todo → In Progress → Done). Plane's API is genuinely pleasant to work with: API key auth, cursor pagination, sensible rate limits with X-RateLimit-Remaining headers. Ten minutes and you're productive.
In Plane, workflow states are UUIDs, not strings. "Done" isn't "done" — it's something like 047f53a9-b079-481a-a28c-dd9f2b3c7fd1. So my worker's first step is roughly:
GET /api/v1/workspaces/{slug}/projects/{project_id}/work-items/?state=<state-uuid>
The docs say the list endpoint "supports filtering, ordering, and field selection through query parameters." Reasonable to assume ?state=<uuid> filters by state.
It does not. It returns every work item in the project, in every state, exactly as if you'd passed nothing at all.
Why this is the worst kind of bug
If that endpoint had returned 400 Bad Request with "unknown parameter: state", I'd have fixed it in thirty seconds and never thought about it again.
Instead it failed open. My script branched on the assumption that "the API returned these, therefore these are actionable." The only reason nothing went sideways is that the specific run I audited happened to have all five items already in Done, and the worker re-checked before touching anything. A different night, a different board state, and I could've had a script confidently "processing" completed work, re-opening items, or double-delivering results. All while every HTTP response looked perfect.
This is the failure mode I now fear more than any error code: the confident, well-formed lie. Errors are honest. A 200 OK with silently-ignored parameters is a betrayal.
The fix (two layers)
Layer one: never trust, always verify. The worker now treats server-side filtering as a hint, not a contract. After any filtered query, it asserts client-side that every returned item actually matches:
ACTIONABLE_STATES = {TODO_ID, IN_PROGRESS_ID, BACKLOG_ID}
items = plane.list_work_items(project_id, state=TODO_ID)
actionable = [i for i in items if i["state"] in ACTIONABLE_STATES]
if len(actionable) != len(items):
log.warning("API filter lied: got %d items, %d actually actionable",
len(items), len(actionable))
Three lines. That's the whole fix, and it's saved me from an entire category of bug. If your automation's correctness depends on a filter, the filter's output must survive a client-side assertion. Non-negotiable now.
Layer two: use the filter grammar that actually exists. To be fair to Plane, the platform does have a real filtering engine — it's just not the naive query parameter I guessed at. There's a structured filters JSON grammar (think {"and": [{"state_group__in": ["started"]}]} with or/not nesting), an advanced-search endpoint that accepts it, and an official Python SDK that exposes the same engine — including a little query language where you can write things like priority = "urgent" AND assignee = currentUser(). Filtering by state_group (backlog, unstarted, started, completed, cancelled) is usually what you want anyway, since it survives state renames and new custom states.
The catch? The documentation for all this is thin enough that Plane's own community forum has a staff reply — from this spring — admitting "the filters documentation is indeed missing examples" and posting the syntax in a comment thread. Which is exactly how I ended up guessing at a query param in the first place.
The API design sin at the root of this
Zooming out, this is Postel's law — "be liberal in what you accept" — biting someone in 2026. Liberal acceptance made sense for HTML parsers in the 90s. For APIs consumed by scripts and agents, it's a trap.
Here's my opinionated take: silently ignoring an unrecognized parameter is a bug, full stop. If a client sends ?state=xyz and your endpoint doesn't support that parameter, the correct responses are:
400 Bad Request: unknown parameter "state", or- Support it. It's an obvious thing to want.
What's never correct is option three: pretend nothing happened and return an unfiltered dataset. The client asked for a subset; you returned a superset; the contract was violated in the dangerous direction. Stripe-style APIs figured this out years ago — send them a typo'd parameter and they'll reject the request with a helpful error. That's not pedantry. That's respect for the machines on the other end.
And as a consumer, the mirror-image rule: a filter is a request, not a guarantee. Any time your code branches on "the API only returned X," add the assertion that proves it. Log the mismatch when it fails. The two minutes you spend writing that check will pay for itself the first time a provider changes behavior, a proxy eats a parameter, or — like me — you guess at syntax that doesn't exist.
The takeaway
The overnight alert that started all this was a false alarm, and I'm weirdly grateful for it. Without that audit, the lying filter would've sat in my worker for months, one bad board state away from a real mess.
So, homework for anyone running automation against someone else's API: pick your most important filtered query right now, and check whether the response actually honors the filter. Don't read the docs and nod — send the request and look.
When was the last time you verified that a 200 OK was telling you the truth?



