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.
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:
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:])48 fields ['tools', 'allowed_tools', 'system_prompt', 'mcp_servers'] ['session_store', 'session_store_flush', 'load_timeout_ms', 'task_budget']
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:
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 None permission_mode None allowed_tools [] set by hand 4
max_turnsisNone, which is the no-limit case the agent loop page describes: the loop runs until Claude stops on its own.permission_modeisNonetoo, so the session falls back to Claude Code's own default mode rather than to a value the dataclass holds.allowed_toolsis an empty list, notNone. 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.
ShopModelstands 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.ShopDeskstands in for the Claude Code binary. It subclasses the SDK's ownTransportbase class, which has six methods:connect,is_ready,write,end_input,closeandread_messages. The SDK writes JSON lines intowriteand reads message dictionaries back out ofread_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?"}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
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__)SystemMessage AssistantMessage ResultMessage
- 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
optionsobject 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:
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)['hooks', 'subtype'] initialize 4 ['Hello']
- The opening request carries two keys, its own subtype and the hooks. No other field of the options object is in it.
hooksisNonebecause 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_turnsreached 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 | |
|---|---|---|
| Session | Creates a new session by default | Reuses the same session |
| Conversation | A single exchange | Several exchanges in the same context |
| Connection | Managed for you | You connect and disconnect |
| Streaming input | Supported | Supported |
| Interrupts | Not supported | Supported |
| Continuing a chat | By hand, through the resume options | Automatic |
| Built for | One-off tasks | Continuous 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.
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.Related
- Previous: The messages you get back
- Next: The result message
- Reference: Configure your agent
- Reference: ClaudeAgentOptions
- Change the question to
"Hello"and read what the stand-in answers instead. - Print
messageitself rather than its type name, and find the text inside the assistant message. - Add
model="claude-sonnet-5"to the options and printdesk.options.modelto see that the transport was handed it.
Every expert started right here.