You have used Claude Code in a terminal. It writes code, fixes bugs and answers questions. Maybe that is the only Claude product you have used. This page is a ladder. It takes you from "I use a coding assistant" to "I design agent systems". You climb one step (rung) at a time. Each rung has a short story that explains why the next thing exists.
Why take a certification at all?
Here is an honest answer for two different readers.
If you are newer: when you learn from random blog posts, you collect separate pieces of knowledge with hidden gaps between them. You may know some prompting tricks but not why the agent loop stops. You may have heard "MCP" but not be able to say what problem it solves. A certification blueprint (the official list of exam topics) is a map of the whole area. It tells you what a complete practitioner knows. Your learning then follows a plan instead of moving at random. If you study the blueprint properly, the certificate comes as a side result. The real value is the structure.
If you are experienced: you probably know 70% of this well, but you have never named the other 30%. The blueprint shows you your blind spots. For most experienced engineers these are MCP primitives, how caching works, and the official vocabulary. In interviews, "I build this every day" plus a current certificate is stronger than either one alone. The certificate is independent proof that your experience matches the shared language of the industry.
In both cases: the exam changes about once a year (the certificate is valid for 12 months), because the field moves fast. An architect treats it as a regular refresh habit, and that habit is itself part of the job.
Rung 1 Β· What is actually happening when you use Claude Code?
You already know this story. You type "fix the failing test". Claude Code searches (greps) the codebase, reads files and edits one. Then it runs the test again and reports success.
What you saw was an agent loop. It is the most important concept in the whole certification:
flowchart TD
Y[You: fix the failing test] --> M[π§ Claude decides: run the test first]
M --> T[Claude Code executes: npm test]
T --> O[Output appended to the conversation]
O --> M2[π§ Claude decides: read auth.ts]
M2 --> T2[Executes: Read auth.ts]
T2 --> M3[π§ β¦edit β rerun β done]
Notice two things. The whole exam depends on them:
Claude never ran anything. The model only asked for each action. Claude Code's own program (the harness) checked permissions and then ran the action. The model decides; the harness executes.
The loop stopped because a condition was met: the test passed. A loop needs stop conditions, budgets and gates (checks that allow or block an action). That is what "architecture" means here.
π Story: the day the loop needs a limit
A developer asks an agent to "clean up the repo." Twenty minutes later the agent is still working. It has moved folders, rewritten half the README, and is now "improving" tests that were already fine. Nothing bad was intended. The loop simply had no limits: no budget (maximum turns or cost), no scope gate (which paths it may not touch), and no definition of "done".
Every architecture topic in Domain 1 (budgets, gates, human approval, stopping) exists because of afternoons like this one. When the exam asks "how do you prevent X", it is asking where you would put the limit.
Rung 2 Β· The same brain, called from Python
This change in thinking makes everything below easier: Claude is a thinking partner, not a vending machine. You bring everything the model cannot supply by itself: the context, the objective, the stage, the goals and the rules. Claude Code feels magical because Anthropic already built all five of these for coding work. The rest of this ladder teaches you to build them yourself, for any kind of work.
Claude Code is one harness (the software that runs and controls the model) around the model. The Claude API lets you build your own harness. It is Anthropic's Messages endpoint: the HTTPS service that carries every model call in this guide. It does not matter whether Claude Code makes the call or your Python script does. Here is the whole secret, the Claude API call under everything else:
PYTHON
import anthropic
client = anthropic.Anthropic() # ANTHROPIC_API_KEY in env
# Abridged: commit_log is a placeholder for your real commit text.
resp = client.messages.create(
model="YOUR_SUPPORTED_MODEL",
max_tokens=1024,
system="You are a concise release-notes writer.",
messages=[{"role": "user",
"content": "Summarize these commits:\n" + commit_log}],
)
print(resp.content[0].text)
Not supplied in this example β defaults and disabled features
π Production shape: release notes from real commits
In production your code reads the real commit log (here with git log), and the model id comes from configuration. Your code also checks stop_reason before it uses the text.
PYTHON
import os
import subprocess
import anthropic
MODEL = os.environ.get("CLAUDE_MODEL", "claude-sonnet-5")
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY from the environment
def read_commit_log(since_tag: str) -> str:
"""Return one line per commit since the given tag."""
return subprocess.run(
["git", "log", f"{since_tag}..HEAD", "--pretty=format:%h %s"],
capture_output=True, text=True, check=True,
).stdout
def write_release_notes(since_tag: str) -> str:
commit_log = read_commit_log(since_tag)
if not commit_log.strip():
return f"No changes since {since_tag}."
resp = client.messages.create(
model=MODEL,
max_tokens=1024,
system="You are a concise release-notes writer.",
messages=[{"role": "user",
"content": "Summarize these commits:\n<commits>\n" + commit_log + "\n</commits>"}],
)
if resp.stop_reason == "max_tokens":
raise RuntimeError("Notes were cut off: raise max_tokens or send fewer commits.")
if resp.stop_reason == "refusal":
raise RuntimeError("The model declined; a person must review this run.")
return "".join(b.text for b in resp.content if b.type == "text")
if __name__ == "__main__":
print(write_release_notes(os.environ.get("SINCE_TAG", "v1.0.0")))
Not supplied in this example β defaults and disabled features
Where it lives: scripts/release_notes.py, run as one CI step after each merge.
That is all. Claude Code, the Claude apps and the agent products built on Anthropic's models are all programs that do three things. They build a messages array. They call this endpoint (or a supported Bedrock/Vertex route). Then they do something with the response.
π§ One page describes every field of that request and of the response it returns: The Messages API Spec. Read it once, and every SDK call in this guide becomes easy to read. The SDK is only a convenient layer over those two JSON shapes.
Why would you move from Claude Code down to the Claude API? Here is the story. Your team likes that Claude Code writes release notes when someone asks. Now you want release notes created automatically on every merge, inside CI (your continuous-integration pipeline), with no person at a keyboard. That is no longer an assistant. It is a feature of your product. You build features on the Claude API (or on the Agent SDK, see rung 4).
Rung 3 Β· Giving the model hands β tools
You reach the model only through the Claude API, and the model can only talk. To let it act, you declare tools. A tool is a JSON description of a function that your code implements. The model requests the tool. Your code runs it. Your code then sends the result back to the model.
PYTHON
# Abridged/pseudocode: MODEL and my_real_lookup() are placeholders for your model id + lookup function.
tools = [{
"name": "get_order_status",
"description": "Look up one order's shipping status by order id.",
"input_schema": {"type": "object",
"required": ["order_id"],
"properties": {"order_id": {"type": "string"}},
"additionalProperties": False}
}]
resp = client.messages.create(model=MODEL, max_tokens=500, tools=tools,
messages=[{"role": "user", "content": "Where is order A-1042?"}])
# resp.stop_reason == "tool_use" β the model WANTS the tool run:
block = next(b for b in resp.content if b.type == "tool_use")
result = my_real_lookup(block.input["order_id"]) # β YOUR code acts
# β¦return a tool_result message, call the Claude API again, get the final answer.
Not supplied in this example β defaults and disabled features
π Production shape: typed function plus tool_runner
You do not write the input_schema dict. The SDK builds it from the decorator's name and description and the parameters' Annotated type hints, so you pass the function, not a dict. tool_runner runs the loop for you: it calls your function, sends the tool_result back, and calls the API again until the model ends its turn. If your function raises an exception, the runner sends the error back as a tool_result with is_error: true.
PYTHON
import os
from typing import Annotated
import anthropic
from anthropic import beta_tool
from pydantic import Field
from app.orders import service as orders # your own code: repository / service layer
MODEL = os.environ.get("CLAUDE_MODEL", "claude-sonnet-5")
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY from the environment
@beta_tool(name="get_order_status", description="Look up one order's shipping status by order id.")
def get_order_status(order_id: Annotated[str, Field(description="The order id, e.g. A-1042.")]) -> str:
return orders.get_status(order_id) # YOUR code acts; raise on failure
def answer(question: str) -> str:
runner = client.beta.messages.tool_runner(
model=MODEL,
max_tokens=500,
tools=[get_order_status], # the function itself, not a dict
messages=[{"role": "user", "content": question}],
max_iterations=5, # hard stop for a runaway loop
)
final = runner.until_done() # the last message the model returned
if final.stop_reason != "end_turn": # tool_use = limit hit; max_tokens; refusal
raise RuntimeError(f"Agent did not finish: {final.stop_reason}")
return "".join(b.text for b in final.content if b.type == "text")
Not supplied in this example β defaults and disabled features
What the SDK generates from the decorated function (check with get_order_status.to_dict()):
JSON
{
"name": "get_order_status",
"description": "Look up one order's shipping status by order id.",
"input_schema": {
"type": "object",
"properties": {
"order_id": {"type": "string", "description": "The order id, e.g. A-1042.", "title": "Order Id"}
},
"required": ["order_id"],
"additionalProperties": false
}
}
Where it lives: the tool in app/tools/orders.py, the runner call in app/agent.py.
This cycle (the model requests, your code runs, your code returns the result) is the agent loop from Rung 1. Now it runs in your own code. You have now seen the topics behind 27% of the exam.
Clarified
π¬ explain this 'block = next(b for b in resp.content if b.type == "tool_use")', I don't see resp.stop_reason call but b.type is confusing also 'next function call?'
Answer
Good question β this one line packs three separate ideas (response shape, block types, and a Python idiom), and the section abbreviates the stop_reason check into a comment, which is why it looks like it's missing. Let me unpack it.
1. The mental model: a response is a list of blocks, not a string
When you call client.messages.create(...), resp is a Message object. Its content field is not a string β it's a list of content blocks, each with a type. That's the whole reason b.type exists.
PYTHON
resp.content == [
{"type": "text", "text": "Let me look that up."}, # β optional prose
{"type": "tool_use", "id": "toolu_01β¦", "name": "get_order_status",
"input": {"order_id": "A-1042"}}, # β the request
]
So b is just the loop variable β each block in turn β and b.type asks "is this block a text block or a tool_use block?" (the SDK gives you attribute access, so b.type in Python == "type" key in the raw JSON).
2. Where's resp.stop_reason?
It's in the comment, not in the code β the snippet is abridged. The line
PYTHON
# resp.stop_reason == "tool_use" β the model WANTS the tool run:
is the condition you'd actually branch on in real code. Un-abridged, it looks like:
PYTHON
if resp.stop_reason == "tool_use":
block = next(b for b in resp.content if b.type == "tool_use")
...
elif resp.stop_reason == "end_turn":
# model is done β print the text block(s)
Two different things at two different levels:
Field
Lives on
Meaning
stop_reason
the Message (resp.stop_reason)
why generation stopped β "tool_use" means "I'm pausing because I need you to run something"
type
each block (b.type)
what kind of block this is β "text" vs "tool_use"
Exam trap:stop_reason is a Message-level field; type is a block-level field. Questions will ask "how does your code know the model wants a tool run?" β the answer is stop_reason == "tool_use", not "look for a tool_use block." (In practice both hold, but the stop_reason is the signal the spec defines.)
3. The next(...) idiom
PYTHON
block = next(b for b in resp.content if b.type == "tool_use")
Read it inside-out:
(b for b in resp.content if b.type == "tool_use") β a generator expression: lazily yields only the blocks whose type is tool_use. (Same shape as a list comprehension, but with () instead of [], so it doesn't build a list.)
next(...) β pulls the first item from that generator and stops.
So the whole line means: "give me the first tool_use block in the response." It's the Python equivalent of resp.content.find(b => b.type === "tool_use") in JavaScript.
Two gotchas with next:
If there's no matching block, next raises StopIteration. Safer: next((b for b in resp.content if b.type == "tool_use"), None) β the second argument is a default.
It only returns one block. That's fine for a teaching snippet, but see the trap below.
4. What you then do with block
Once you have it, three fields matter β and all three come from the section's snippet or the tool_result step it describes:
PYTHON
block.name # "get_order_status" β which of YOUR functions to dispatch to
block.input # {"order_id": "A-1042"} β validated against your input_schema
block.id # "toolu_01β¦" β you MUST echo this back as tool_use_id
PYTHON
result = my_real_lookup(block.input["order_id"]) # β YOUR code acts
messages.append({"role": "assistant", "content": resp.content}) # echo the model's turn
messages.append({"role": "user", "content": [{
"type": "tool_result",
"tool_use_id": block.id, # β pairs the answer to the request
"content": str(result),
}]})
# β¦call the Claude API again with the same tools β final answer, stop_reason "end_turn"
That round trip β tool_use block out, tool_result block back in, keyed by id β is the agent loop the section says is "27% of the exam."
The trap the exam sets around next(...)
next grabs only the firsttool_use block. But the model can emit severaltool_use blocks in one response (parallel tool calls). The rule: every tool_use block needs a matching tool_result in the very next user message, or the API rejects the turn. Production code loops:
PYTHON
tool_results = []
for b in resp.content:
if b.type == "tool_use":
tool_results.append({"type": "tool_result", "tool_use_id": b.id,
"content": str(dispatch(b.name, b.input))})
If a question shows a next(...)-style single-block handler and asks "what breaks?", the answer is multiple tool calls in one turn β orphaned tool_use blocks.
Memory hook:stop_reason says WHY the model stopped (Message level); type says WHAT each block is (block level); next grabs the first β the loop grabs them all.
The API deliberately makes content a list of typed blocks rather than a string precisely so text and tool requests can coexist in one turn β a text block ("Let me checkβ¦") followed by a tool_use block is a normal, single response.
stop_reason is the control-flow signal and content is the payload β keep them separate in your head. The exam loves asking which one drives the loop (stop_reason), and which one you parse to know what to run (content).
The tool_use_id β tool_result.tool_use_id pairing is the contract that lets the model correlate answers to requests when it fired several tools at once; that's why block.id matters as much as block.input.
βββββββββββββββββββββββββββββββββββββββββββββββββ
Recap:resp.content is a list of typed blocks; b.type == "tool_use" picks the ones that are tool requests; next(...) returns the first such block (raises if none, ignores the rest). The stop_reason == "tool_use" check is present as a comment because the snippet is abridged β in real code it's the if that gates this whole branch. For the exam, remember stop_reason lives on the Message, type lives on each block, and production code loops over alltool_use blocks and returns one tool_result per tool_use_id.
π¬ does all tools needs to be in the python functions, including all the mcps, many times organization may develop several tools shared by all teams and projects, how do we pull shared tools from tools repo or something because production systems we can simply define tools where we make the llm calls.
Answer
Short answer: no. Rung 3's Python function is a placeholder for "your code executes." The exam mental model is that every tool has two separable halves, and neither half has to live in the file where you call client.messages.create.
The mental model: declaration vs. execution
Half
What it is
In Rung 3
Where it can live
Declaration
The JSON in tools β name, description, input_schema
the tools = [{...}] literal
any importable package, a JSON file, a registry, or published by a server
Execution
Whatever handles the tool_use block after stop_reason == "tool_use"
my_real_lookup(block.input["order_id"])
same process, a shared library, a remote service, an MCP server, or Anthropic's servers
The invariant the exam tests: the model only ever emits a tool_use request and consumes a tool_result. It never executes anything. Who executes is an architecture decision β that's exactly the Rung 3 β Rung 5 progression on this page ("when your tools need to be plugs, not wiring").
The four ways to share tools across teams
1. Shared library / tool registry(same pattern as Rung 3, just packaged)
An internal package exports (definition, handler) pairs; each app builds tools=[t.definition for t in registry] and dispatches tool_use.name β handler. Cheapest, but every consumer must be in the same language and redeploy on every tool change. Fine for one team; brittle org-wide.
2. MCP server(the org-level answer β Rung 5, beyond this section)
Both halves move behind a server. The server publishes schemas (tools/list) and executes calls (tools/call); any client β Claude Code, the Agent SDK, or your own API code β discovers them at runtime. Nothing is hand-declared in your tools array. Consumers:
Claude Code: .mcp.json in the repo (project-scoped, checked in β so the whole team gets the same tools); they appear namespaced as mcp__<server>__<tool>.
Agent SDK: mcpServers in options.
Messages API directly(beyond the section, from the API reference): the MCP connector needs both halves β mcp_servers=[{type:"url", url, name}]andtools=[{type:"mcp_toolset", mcp_server_name:<same name>}], with beta mcp-client-2025-11-20. Sending mcp_servers alone is a validation error.
3. Anthropic server-side tools(beyond the section)web_search, web_fetch, code_execution β you declare a type, write zero handler code, and the result comes back in the same response. No loop iteration on your side.
4. Tool search for large shared catalogs(beyond the section)
When the org catalog is hundreds of tools, mark them defer_loading: true and add tool_search_tool_regex_20251119 / _bm25_20251119 so only relevant schemas enter context. Trap: at least one tool must stay non-deferred, or the API returns 400.
The traps the exam sets
"MCP replaces tool use." No β MCP is a discovery and transport protocol for tools. The model still emits tool_use; MCP just decides who publishes the schema and who runs the call.
"Tools must be Python functions." The input_schema is language-neutral JSON; the handler is anything that returns a tool_result. A Go service, a SQL query, a curl to another team's API all qualify.
"Define tools where you make the LLM call" is wrong. It's not wrong β it's the right first step (Rung 3) and the wrong last step once tools are shared. The exam wants you to pick MCP precisely when the same tool serves multiple clients/teams (Domain 4, 18%).
Memory hook:Rung 3 is wiring, Rung 5 is plugs. Wiring when one app owns the tool; plugs (MCP) when the org does.
The reason Rung 3 says "you've seen 27% of the exam" is that the loop is invariant β every sharing option above only changes who fills in the tool_result, never the tool_use β tool_result cycle itself.
Prompt-caching angle worth remembering for Domain 5: tools renders first in the cache prefix, so a shared registry that returns tools in nondeterministic order silently kills your cache hits across every consumer.
MCP's tools/list is why schemas can't drift between teams β the server is the single source of truth, which is the maintainability argument the exam scenarios reward.
βββββββββββββββββββββββββββββββββββββββββββββββββ
Recap: Rung 3's Python function is illustrative, not a requirement β tool declaration (JSON) and execution (any code) are separable. For org-shared tools the choices are a shared library/registry (simple, same-language), an MCP server (the exam's intended answer, Rung 5), Anthropic server-side tools, or tool search for big catalogs. Defining tools inline at the call site is correct for a single app and becomes the trap answer once multiple teams consume the same tools.
Rung 4 Β· The Agent SDK β Claude Code's engine, as a library
Writing the loop, permissions, file tools and retries yourself becomes tiring. The Claude Agent SDK (Python/TypeScript) is the same engine that Claude Code runs on, packaged as a library for your programs. It includes the loop, the Read/Write/Bash/Grep tools, permissions, hooks, subagents and MCP.
When do you use the Claude API, and when the SDK? Here is the story version:
You are building a support bot for your product that reads your docs and creates tickets β Agent SDK (you want the SDK to manage the loop and tools for you).
You are adding one AI feature, such as "summarize this page" or "classify this email" β plain Claude API (one call, no loop needed; the SDK would be like using a truck to buy a few groceries).
You are at a terminal writing software β Claude Code itself.
The model is the same at every level. You only choose how much of the harness you want already built.
The decision table (save this; it answers "SDK or API?" for every project):
Signal in your project
Build on
There is a loop: the model decides the next steps, requests tool calls again and again, and runs over many turns
Agent SDK. It provides the loop, tool execution, permissions, subagents, hooks, an MCP client and context management (including compaction)
It is a single call: classify, extract, summarize, or return one answer in a fixed shape
Claude API. An agent engine here is like a truck for a few groceries
A workflow: a few calls in a chain, with checks between them (Domain 1's patterns)
Claude API. Chaining calls is ordinary code; you need no engine
Large offline volume, such as 10,000 documents overnight
Claude API's batch API
You need unusual loop behavior, such as research on custom orchestration or your own framework
Claude API. Owning the loop is the project
Your language is not Python/TypeScript
Claude API (the Agent SDK exists only for those two; every other language uses HTTPS)
One point confuses many people. pip install anthropic gives you the client library. It is a thin helper layer (client.messages.create(...)), and using it still counts as "using the Claude API" in every sense above: your code owns the loop. The Agent SDK is a different, higher-level product: the engine. There are three layers, from the bottom up: raw HTTPS β client library (nicer syntax, same responsibilities) β Agent SDK (the loop machinery, already built).
The warning sign: you may notice that you are writing tool dispatch, permission checks or compaction logic by hand. Then you are rebuilding the Agent SDK yourself, one bug at a time. Stop and use the SDK. The opposite warning sign: if your "agent" never loops, you did not need an agent engine. You needed one well-designed API call. (Mixing both is normal and good. For example, an agent runs on the SDK, and its harness also makes cheap raw-API calls for small classification tasks.)
Rung 5 Β· MCP β when your tools need to be plugs, not wiring
At Rung 3 you connected one tool to one program by hand. Now the story grows. Your company has a CRM, a ticket system and a wiki. You want all three in Claude Code, in Claude Desktop, and in the support bot you built with the SDK. Connecting them by hand means 3 systems Γ 3 apps = 9 integrations, and you must maintain each one forever.
MCP (Model Context Protocol) removes this multiplication. Each system provides one MCP server. Each app already speaks MCP as a client. That makes 3 + 3 = 6 pieces, and any new app gets all the servers at once. Think of it as USB-C for AI tools.
"Is MCP just a REST API?" β the comparison everyone needs
REST API
MCP server
Designed for
programmers who read docs and write connecting code
models that find and call capabilities while running
Discovery
a programmer reads the docs and hardcodes the endpoints
the client asks the server "what tools do you have?"; the server describes itself
Descriptions
written for humans
written for the model; the description IS how Claude decides when to use the tool
Connection
stateless requests
a stateful session: first an initialization handshake, then the agreed capabilities stay in place for that connection
Extras
only endpoints
tools + resources (context data) + prompts (templates). Each has a defined controller: the model controls tools (it chooses them but does not run them), the app controls resources, and the user invokes prompts
Relationship
MCP servers often wrap REST APIs
the adapter that lets a model use an API
An MCP server is often a wrapper of about 100 lines around an existing REST API. Its value is the standard plug, not new functions.
Rung 6 Β· From "it works" to "it's an architecture"
The last rung is the real subject of the exam. You take rungs 1β5 and make them safe, cheap and reliable. That means guardrails the model cannot get around, budgets that stop an agent that runs out of control, caching that cuts costs by 90%, evaluations (evals) that catch regressions, and human approval wherever money moves. Those are Domains 1β5. You now have enough background to read them in order.
Your path from here
You now have the ladder in your head. So where do you start? Find the row that describes you and follow it:
Exam Guide β skim domains for π― exam-lens / πͺ€ trap callouts β Practice β patch gaps
π― Tip for every path: terms with a dotted underline in this guide are clickable. Each one opens a card with the definition, how it looks in code, and a story about when to use it. Use them often; that is why they exist.