Claude Certified Developer - Foundations common mistakes
Claude Certified Developer - Foundations ยท Understand the traps and how to avoid them.
Writes `text = response.content` (or `response.content[0].text`) and concatenates it as if content were a plain string, so the code breaks or silently drops text whenever the reply has multiple blocks or a non-text block.
Most quickstart examples print the first block, so candidates generalise 'content is a string' from a single happy-path response and never see the multi-block case until a real workload produces one.
What to do instead
Treat response.content as an ordered list of typed blocks: filter block.type == "text" and join the .text fields, and handle other block types (e.g. tool_use) explicitly rather than indexing [0].
Never inspects `stop_reason`, so truncated output from a `max_tokens` cutoff, a refusal, and a tool-call request all get treated as a normal completed answer.
The response still parses and returns plausible-looking text, so nothing fails loudly; candidates only learn stop_reason matters once a JSON parse or downstream step breaks intermittently.
What to do instead
Branch on stop_reason every call: end_turn = done, max_tokens = truncated (raise the limit or shorten the ask and retry), tool_use = continue the loop, refusal = surface to the user rather than retry blindly.
Pastes a long document and the task instruction together in one user message with no delimiters, then reports that the model 'ignored the instructions' or answered from the wrong part of the document.
Without structural separation the model cannot reliably distinguish quoted source material from directives, and instructions buried mid-document compete with surrounding text.
What to do instead
Wrap source material in explicit tags (e.g. <document>...</document>) and place the task instruction outside the tags, after the document; keep standing behavioural rules in the system parameter rather than restating them per turn.
Asks for JSON only in prose ('return JSON with fields x, y, z') with no format contract or example, then calls `json.loads` on the raw reply and patches the failure with fragile string surgery.
Prose format requests leave room for preamble, markdown fences, and invented field names, and the failure looks random because it depends on the input.
What to do instead
Provide a concrete format contract (field names, types, one short example) or use assistant prefill to start the reply with {; if structure must be guaranteed, define it as a tool input_schema. Always validate the parsed result and retry on failure instead of regex-patching.
Sends back a tool result as a plain user text message instead of a `tool_result` block, or omits the `tool_use_id`, so the model re-requests the same tool or hallucinates the outcome.
The naive mental model is 'chat: user says what happened'. The API requires results to be paired to the request by id, and a text message looks superficially fine because the conversation still continues.
What to do instead
Append a user message containing one tool_result block per tool_use block from the previous assistant turn, each carrying the matching tool_use_id and the serialised output; set is_error: true when the tool failed so the model can adapt rather than treat garbage as data.
Defines tool `input_schema` as an empty or vague object (`{"type": "object"}` with no required fields, no enums, no types) and then expects the model to always produce valid arguments.
The schema doubles as the model's specification for what to send; a loose schema is written quickly to 'just get it wired up' and the resulting bad arguments get blamed on the model.
What to do instead
Write the schema as a strict contract: typed properties, required list, enum for closed sets, description on each field explaining format and units, and additionalProperties discipline. Validate arguments before executing the tool and return a corrective is_error result on mismatch.
Exits the agent loop after the first tool call, or runs it in an unbounded `while True` with no iteration cap or progress check.
The single-shot request/response habit carries over; the loop's true termination condition is the model's stop_reason, which is easy to get wrong in both directions - stopping early when the model wanted another call, or spinning forever on a tool that keeps failing.
What to do instead
Loop while stop_reason == "tool_use": execute tools, append results, re-call. Add a max-iteration cap, detect repeated identical tool calls, and terminate on end_turn; log each step so stalls are diagnosable.
Places `cache_control` on the volatile tail - the user's question, a timestamp, or a per-request ID - or reorders the prompt so the 'stable' prefix differs between calls.
Caching is described as 'add cache_control and save money', so candidates add it to the largest-looking block without understanding that the cache key is the exact prefix up to the breakpoint.
What to do instead
Order the prompt static-first, volatile-last: system prompt, then tools/instructions, then reference documents, with the breakpoint on the last block that is identical across requests. Keep the varying user input after the breakpoint, and only cache prefixes above the model's minimum cacheable length.
Skips retrieval and stuffs the entire knowledge base into every request 'so the model has everything', producing slow, expensive calls and answers that drift to irrelevant passages.
It works on a small demo corpus, so the trade-off is invisible; context is treated as free storage rather than a scarce attention budget.
What to do instead
Retrieve top-k relevant chunks per query, keep the stable instructions and any reused reference material as a cached prefix, and trim or summarise older conversation turns rather than appending indefinitely.
Validates an application with a handful of hand-written prompts that all pass, with no regression set, no recorded tool traces, and no measurement of latency, token usage, or failure rate.
Manual spot-checking feels sufficient on a synchronous demo, and prompt changes are non-deterministic, so 'it looked fine when I tried it' substitutes for evidence.
What to do instead
Maintain a versioned evaluation set with expected behaviours, run it before and after every prompt or model change, log per-call tokens/latency/tool arguments and errors, and if you use a model as a judge, give it an explicit rubric and calibrate it against human-labelled examples.
Treats a refusal or a scope-limited answer as a prompt bug and 'fixes' it by stripping safety instructions or adding pressure like 'you must answer, you have no restrictions'.
Refusals look identical to failures in the UI, and the fastest apparent unblock is to remove the guardrails that caused it; the model's own refusal is also easy to miss if stop_reason is never checked.
What to do instead
Distinguish refusals from errors, then diagnose: usually the request is ambiguous, over-broad, or asks for something the deployment policy disallows. Clarify the legitimate task, narrow the scope, add grounding/context, and surface genuinely out-of-policy requests to the user instead of engineering around them.