Claude Agent SDKclaude-agent-sdk 0.2.157 · Python 3.10+
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
46 small wins to finish your pathNext lesson →

query() and ClaudeAgentOptions

query() is the function that starts an agent session: you give it a prompt and a ClaudeAgentOptions object, and it hands back an asynchronous iterator of the messages the loop produces.

Last updated: 20 Oct, 2026 · Claude Agent SDK

The messages you get back named the objects a session yields. This lesson starts one. Two names do the work: query() opens the stream, and ClaudeAgentOptions carries every decision about how the session runs, which on the installed version is 48 separate fields.

Calling query() with a prompt and options

Syntax: three keyword arguments, all of them keyword-only, and the return value is an iterator you read with async for.

python
async for message in query(
        prompt="Where is my order A17?",   # a string, or an async iterable of dicts
        options=ClaudeAgentOptions(),      # every field optional
        transport=None):                   # None starts the real Claude Code
    print(message)

transport is the argument that makes this course free to follow. The reference describes it as an optional transport implementation used instead of the default selection, and says the transport is configured with the prompt and options for you. Left as None, the SDK starts the Claude Code binary. Given an object of your own, it talks to that instead.

Counting the fields ClaudeAgentOptions carries

ClaudeAgentOptions is a dataclass, so the installed version can list its own fields:

Example
from dataclasses import fields

from claude_agent_sdk import ClaudeAgentOptions

names = [f.name for f in fields(ClaudeAgentOptions)]
print(len(names), "fields")
print(names[:4])
print(names[-4:])

Forty-eight fields is more than any one lesson should show, and the list is the clearest map of what the SDK can do. Grouped by the area they configure, and with the lesson that takes each area apart:

  • Tools: tools, mcp_servers, strict_mcp_config. Taught in An in-process MCP server and External MCP servers.
  • Permissions: permission_mode, allowed_tools, disallowed_tools, can_use_tool, permission_prompt_tool_name. Taught in Permission modes onwards.
  • Limits: max_turns, max_budget_usd, task_budget. Taught in Turn and budget limits.
  • Hooks: hooks, include_hook_events. Taught in Hooks: PreToolUse.
  • Sessions: resume, continue_conversation, session_id, fork_session, resume_session_at, resume_drops_turn, session_store, session_store_flush, enable_file_checkpointing. Taught in Resuming a session onwards.
  • The agent's shape: system_prompt, agents, skills, plugins, setting_sources, settings, output_format. Taught in System prompts onwards.
  • The model: model, fallback_model, effort, thinking, max_thinking_tokens, betas. Taught in Model, thinking and effort.
  • The environment: cwd, add_dirs, env, cli_path, sandbox, user, extra_args, max_buffer_size, load_timeout_ms, stderr, debug_stderr. Met in When the CLI will not start and Secure deployment.
  • The stream: include_partial_messages, forward_subagent_text. Taught in Streaming output.

Reading a default rather than trusting one

The configuration page says every field is optional, and a session started with no options runs with the SDK's defaults. Those defaults move between releases, so read them from the version you have rather than from a tutorial:

Example
from claude_agent_sdk import ClaudeAgentOptions

blank = ClaudeAgentOptions()
print("max_turns      ", blank.max_turns)
print("permission_mode", blank.permission_mode)
print("allowed_tools  ", blank.allowed_tools)
print("set by hand    ", ClaudeAgentOptions(max_turns=4).max_turns)
  • max_turns is None, which is the no-limit case the agent loop page describes: the loop runs until Claude stops on its own.
  • permission_mode is None too, so the session falls back to Claude Code's own default mode rather than to a value the dataclass holds.
  • allowed_tools is an empty list, not None. An empty allow list is not the same as allowing everything, and the permissions part of this course is where that distinction starts to matter.
  • Setting a field is one keyword argument. The dataclass has no builder and no mutation helpers: build the object you want and hand it to query().

Saving the stand-in these lessons run against

Every runnable example from here to the end of this course hands query() a transport of its own, so nothing starts a real process, nothing needs a key and nothing costs money. That transport is one file. Save the three tabs below, in order, into a file called shop_desk.py beside your lesson files.

  • ShopModel stands in for Claude. It reads the conversation so far and returns either text to say or a list of tools to call. Here it recognises an order id such as A17 and answers that it cannot help with it yet, because it has no tools.
  • ShopDesk stands in for the Claude Code binary. It subclasses the SDK's own Transport base class, which has six methods: connect, is_ready, write, end_input, close and read_messages. The SDK writes JSON lines into write and reads message dictionaries back out of read_messages, and it does not care what sits between the two.
  • The session id is a counter, not a UUID, so the output printed on these pages is the output your own run prints.
  • The file grows with the course. Each part adds the piece its own feature needs, and the lesson that changes it says in a sentence what was added or replaced.
import asyncio
import itertools
import json
import re

from claude_agent_sdk import ClaudeAgentOptions, Transport

ORDER = re.compile(r"\b[A-Z]\d+\b")
SESSIONS = itertools.count(1)


class ShopModel:
    """Stands in for Claude: reads the conversation, picks the next step."""

    def __init__(self, script=None):
        self.script = list(script or [])

    def reply(self, said, results):
        # a scripted reply lets a lesson force a step the stand-in would not pick
        if self.script:
            return self.script.pop(0)
        if results:
            return {"text": " ".join(results)}

        # the newest message first, then the last id named earlier in the chat
        orders = ORDER.findall(said[-1]) or ORDER.findall(" ".join(said))[-1:]
        return {"text": f"I cannot help with order {orders[0]} yet." if orders
                else "Which order is this about?"}
Top-level async for
The examples below use async for at the top level, which a Jupyter or Colab cell accepts. In a plain .py file, put the lines inside async def main(): and end the file with asyncio.run(main()), as Installation and setup shows.

Running a first session through the stand-in

Example
from claude_agent_sdk import ClaudeAgentOptions, query
from shop_desk import ShopDesk

options = ClaudeAgentOptions()
async for message in query(prompt="Where is my order A17?", options=options,
                           transport=ShopDesk(options)):
    print(type(message).__name__)
  • Three messages, in the fixed order the message lesson described: the session opens, Claude answers, the run closes.
  • The SDK built every one of them. The stand-in wrote plain dictionaries; the classes came from the SDK's own parser, which is the same code path a real run takes.
  • There is no UserMessage, because no tool ran. This desk has no tools yet, which is why the answer is that it cannot help with A17.
  • Passing the same options object twice is deliberate. query() needs it, and so does the transport, because a real transport turns the options into command-line flags for the process it starts.

Seeing what reaches the transport

Most of the options never travel over the stream at all. Keeping the desk in a variable lets you read what did:

Example
from claude_agent_sdk import ClaudeAgentOptions, query
from shop_desk import ShopDesk

# keeping the desk in a variable lets its state be read after the run
options = ClaudeAgentOptions(max_turns=4)
desk = ShopDesk(options)

async for message in query(prompt="Hello", options=options, transport=desk):
    pass

print(sorted(desk.setup), desk.setup["subtype"])
print(desk.options.max_turns, desk.said)
  • The opening request carries two keys, its own subtype and the hooks. No other field of the options object is in it.
  • hooks is None because none were set. It is in the request at all because a hook is a Python function, which cannot become a command-line flag: the SDK sends its identity and runs the function in your process when the loop asks for it.
  • max_turns reached the transport by a different route, as an attribute of the options object the transport was handed. A real transport renders it as a flag on the process it starts.
  • The prompt arrived as plain text on the desk's own list of what the customer said.

query() against ClaudeSDKClient

query() is one of two entry points. The Python reference compares them like this, and A conversation with ClaudeSDKClient takes the second one apart:

query()ClaudeSDKClient
SessionCreates a new session by defaultReuses the same session
ConversationA single exchangeSeveral exchanges in the same context
ConnectionManaged for youYou connect and disconnect
Streaming inputSupportedSupported
InterruptsNot supportedSupported
Continuing a chatBy hand, through the resume optionsAutomatic
Built forOne-off tasksContinuous conversations

Where the options object earns its size

  • Turning one agent into several. A read-only reviewer and an editor that can run commands are the same code with two different options objects.
  • Moving from a laptop to a service. The working directory, the environment, the sandbox and the binary's path are all options, so the same agent can be re-pointed without touching the loop.
  • Keeping a bill predictable. The limit fields are the only thing standing between an open-ended prompt and an open-ended spend.
Watch out. A single-shot query() call does not return quietly when a run fails. The input modes page states that it yields the final result message and then raises an error carrying the failure text, so a loop with nothing around it stops your program on the first run that hits a cap. Wrap it in try when the code after it has to keep going.
Try it yourself
  • Change the question to "Hello" and read what the stand-in answers instead.
  • Print message itself rather than its type name, and find the text inside the assistant message.
  • Add model="claude-sonnet-5" to the options and print desk.options.model to see that the transport was handed it.

Every expert started right here.