Classify each dependency reply into ok, empty, unavailable or skipped with a per-source mapping table
At the adapter boundary for each source, map the raw reply (transport error, status code, body shape, tool output) to ok, empty, unavailable or skipped using an explicit per-source table. Empty needs positive evidence from the source's own contract; every unrecognized signal defaults to unavailable with a reason, never to empty.
Classify each dependency reply into ok, empty, unavailable or skipped with a per-source mapping table
When to use
Use this at the point where one source's raw reply is turned into a status for the caller: the HTTP or RPC client wrapper, the database or search adapter, the tool-result handler in an agent, the file or queue reader. It applies whenever the caller downstream models each source as ok, empty, unavailable or skipped and keeps partial results.
This skill assumes that model already exists. It covers the earlier, narrower question that the model leaves open: given one concrete reply from one concrete source, which state is it, and how do you decide that consistently instead of case by case?
The failure pattern
The state model is only as good as the classification feeding it. The same misclassification keeps reappearing in different clothes:
- Transport status read as meaning. A not-found status is mapped to empty everywhere. On a collection lookup that may be right. On a route behind a gateway during a deploy, or on a resource the caller is not allowed to see, it is an outage or a permission problem wearing an empty answer's clothes.
- Success status with an error body. The source answers with a success status and an error envelope in the body. A handler that checks only the status treats the envelope as data, finds no items, and reports empty.
- Missing field defaulted to empty. The body parses, but the field holding the items is absent because of schema drift or a partial serialization. A default of an empty collection fills the gap, and the absence of the field becomes the absence of data.
- Free-text tool output. An agent tool returns a sentence. A handler looks for the word results, finds none, and concludes there are none. The sentence was an error message.
- Catch-all fallback to empty. Any unrecognized signal lands in a final branch that returns an empty collection because that keeps the caller's type happy.
Each of these produces an empty state without the source ever confirming that there is nothing. The downstream model then trusts it.
Rule
Empty is a positive claim and needs positive evidence from the source's own contract. Unavailable is the default for everything that is not positively ok or positively empty. Classification is written down per source as an explicit table of signal to state to reason code, kept next to that source's adapter and reviewed whenever the source's contract changes.
Three consequences follow:
- A reply may be classified empty only when the source answered on the expected route, in the expected shape, and the shape itself says there are no items.
- A reply may be classified ok only when the body validates against the expected shape and contains at least one item.
- Any signal that is not in the table is unavailable, with a reason code of unclassified, and is logged so the table can be extended. It is never empty.
How to apply
- Write the table before the handler. For each source, list every signal you expect: transport failures, each status code or code family, body validation outcomes, and, for tools, each documented result shape. Give each row a state and a short reason code. Rows for empty must cite the evidence that makes the answer positive.
- Layer the checks in order and stop at the first failure. Transport first (resolution, connection, TLS, timeout, cancellation, circuit open). Then protocol status. Then content type. Then body parse. Then schema validation. Then the item count. A failure at any layer is unavailable with that layer's reason code. Only a reply that passes every layer reaches the ok or empty decision.
- Treat not-found by route class, not globally. Decide separately for each route whether not-found can mean empty. A lookup of one resource by identifier, on a route the adapter has otherwise confirmed working, may map not-found to empty. A collection or search route should return a success status with an empty collection when there is nothing, so not-found there is unavailable. If the not-found body does not match the source's own error format, for example an HTML page or a gateway signature, it is unavailable regardless of route.
- Keep permission problems out of empty. Unauthorized and forbidden are unavailable with an auth reason code. Some sources hide the existence of a resource behind not-found when the caller lacks permission. For those sources, not-found may map to empty only when permission has been established some other way; otherwise map it to unavailable with reason possibly-forbidden.
- Validate the body before counting. Require the item field to be present and of the expected type. A missing or wrongly typed field is unavailable with reason malformed, not empty. A success status carrying the source's error envelope is unavailable with the envelope's error code as the reason.
- Detect partial replies at classification time. Partial content status, a continuation token, a truncated stream, or a page filled to its size cap without an end marker classify as ok or empty with completeness set to false. Do not wait for the aggregator to guess this later.
- Treat rate limits, overload and circuit-open as unavailable. They mean the source refused to look. Give them their own reason codes so retry policy can tell them from hard failures.
- Classify tool results by structure, not by wording. An agent tool may return empty only if its contract returns a structured zero marker: a count field of zero, an empty array, or an explicit no-match flag. Free text that mentions no results is unavailable with reason unstructured unless the tool's contract guarantees that exact string. Tool errors surfaced as text are unavailable with reason tool-error.
- Carry the reason code through. The state and reason code travel with the source outcome to the aggregator, the cache and the consumer. A state without a reason cannot be audited or retried sensibly.
- Log and count classification misses. Every hit on the default unavailable row emits a metric and a log line with the signal that was seen. Review these regularly; each one is either a new table row or a source contract change.
Minimal shape (language-neutral)
classification table for one source = [ { signal: "dns or connect failure", state: unavailable, reason: "transport" }, { signal: "timeout or cancelled", state: unavailable, reason: "timeout" }, { signal: "circuit open", state: skipped, reason: "circuit-open" }, { signal: "status 401 or 403", state: unavailable, reason: "auth" }, { signal: "status 429 or 503", state: unavailable, reason: "overloaded" }, { signal: "status 5xx other", state: unavailable, reason: "server-error" }, { signal: "status 404, item route, body matches source not-found format", state: empty, reason: "not-found-item" }, { signal: "status 404, collection route or foreign body", state: unavailable, reason: "route-missing" }, { signal: "status 200, error envelope", state: unavailable, reason: "envelope-error" }, { signal: "status 200, body fails schema", state: unavailable, reason: "malformed" }, { signal: "status 200, items array length 0", state: empty, reason: "confirmed-empty" }, { signal: "status 200, items array length above 0", state: ok, reason: "ok" }, { signal: "status 206 or continuation token present", state: ok or empty, complete: false, reason: "partial" }, { signal: "anything else", state: unavailable, reason: "unclassified" } ]
Whether circuit-open is skipped or unavailable is a per-source choice. Skipped says the adapter chose not to call; unavailable says it tried. Either way it is not empty.
Reasoned example
A search feature fans out to a product index behind an API gateway. During a rolling deploy the index route is briefly unregistered, and the gateway answers not-found with an HTML body for a few seconds.
With a global rule that not-found means empty: every search during that window reports zero products from the index. The aggregator marks the source empty and complete. A page shows no products. A job that removes search boosts for products with no index hits reads the empty answer and strips the boosts.
With a per-source table: the row for not-found on a collection route maps to unavailable, and the body check fails the source's error format anyway. The index outcome is unavailable with reason route-missing, the aggregate is partial, the page says the product index did not respond, and the boost job sees unavailable and skips the run.
This example is reasoning about the procedure. It is not the result of an executed test.
Checks before shipping
These are suggested verification steps for adopters, not observed results.
- For each row of the table, inject that signal and assert the exact state and reason code. The default row must be reachable by a signal not otherwise listed.
- Return a success status with the source's error envelope. The state must be unavailable, not empty.
- Return a success status with the items field missing. The state must be unavailable with reason malformed.
- Return not-found with an HTML body on an item route. The state must be unavailable, not empty.
- Return a page filled to its size cap with no end marker. The state must be ok with completeness false.
- For an agent tool, return the sentence that no results were found without the structured zero marker. The state must be unavailable with reason unstructured.
- Confirm the reason code survives serialization to the aggregator and to the consumer.
Pitfalls
- A default branch that returns an empty collection to satisfy a return type.
- Checking the status code and never parsing or validating the body.
- Optional field access with an empty default around the items field.
- One shared not-found rule across every route of a source.
- Mapping forbidden to empty because the caller has nothing it is allowed to see.
- Treating a rate limit as a hard failure and a hard failure as a rate limit; both are unavailable, but their reason codes drive different retry behaviour.
- Matching tool output on wording that the tool never promised.
- Extending the handler with a new special case without adding the row to the table, so the table stops describing the code.