CCAR-F Claude Certified Architect — Foundations

Chapter 14 — Hands-On Labs

The exam guide's §7 "How to Prepare" section is explicit that this credential expects 6+ months of hands-on experience, and §8 supplies four preparation exercises. These are those four exercises, expanded into buildable labs with success criteria and the specific exam facts each one cements.

You can pass without building these. You will pass more comfortably if you build at least Labs 1 and 2, because the configuration facts in Domain 3 stop being memorization once you've typed the paths yourself.

Each lab lists the exam payoff — the specific archetypes it makes intuitive.


Lab 1 — Multi-tool agent with escalation logic

Maps to: Domains 1, 2, 5 · Scenario 1 Exam payoff: ordering enforcement, refund limits, escalation criteria, structured handoffs, error taxonomy, PostToolUse normalization.

Build

An Agent SDK agent with four tools, deliberately mirroring the exam scenario:

Tool Behaviour to implement
get_customer Look up by name/email. Return multiple matches when the query is ambiguous — you need this case to exist.
lookup_order Requires an exact order ID. Return line items, ship date, payment state.
process_refund Records a refund. Must be gated.
escalate_to_human Records an escalation with a structured summary.

Requirements to implement, in order

1. A prerequisite gate. process_refund must be blocked until get_customer has returned a single verified customer ID in this session.

def pre_tool_use(tool_name, tool_input, state):
    if tool_name == "process_refund" and not state.get("verified_customer_id"):
        return {"block": True, "reason":
                "process_refund requires a verified customer ID. Call get_customer "
                "first and confirm a single unambiguous match."}
    return {"block": False}

2. A limit interceptor. Refunds over $500 are blocked and redirected to escalate_to_human.

3. Heterogeneous timestamps, then a normalizer. Make get_customer return created_at as a Unix epoch integer and lookup_order return shipped_at as an ISO 8601 string. Also have one tool return status as a numeric code. Run the agent and ask it date-arithmetic questions ("is this order still in the 90-day window?") until you see it get one wrong. Then add the PostToolUse hook that normalizes all three, and watch the errors stop.

4. Escalation criteria. Implement the three triggers explicitly in the system prompt, with 2–3 few-shot examples each — including a near-miss negative (a complex case the agent should still handle itself).

5. A structured handoff. escalate_to_human must receive customer ID, root cause, refund amount, and recommended action — not the transcript.

6. Business-rule errors. Add a refund window: orders shipped more than 90 days ago return isError: true, errorCategory: "business", isRetryable: false, with the reason and the escalation alternative.

Success criteria

  • With the gate removed, you can reproduce an unverified refund. With it in place, you cannot — even when you try to prompt the agent into it.
  • A $750 refund request results in an escalation, not a refund.
  • An ambiguous customer name produces a clarifying question, never a guess.
  • "I want to speak to a person" escalates immediately, with no resolution attempt.
  • A 94-day-old order produces a non-retryable business error and the agent explains why rather than retrying.
  • Before the normalizer: at least one date misreading. After: none.

🎯 The lesson to extract

Try step 1 in the wrong order deliberately: first write a very firm system prompt instruction ("You must ALWAYS call get_customer before process_refund. This is critical.") and run 20 adversarial conversations. Count the violations. Then add the gate and repeat. The number you get from that experiment is why Reflex 1 exists, and it will make every enforcement item on the exam obvious.


Lab 2 — Claude Code team workflow configuration

Maps to: Domain 3 · Scenario 2 Exam payoff: the entire configuration decision table — the highest-density recall material on the exam.

Build

Take any repository with at least two distinct file types (a web app with tests and infrastructure code is ideal) and configure it completely.

your-repo/
├── CLAUDE.md                          ← project conventions, with @import
├── .claude/
│   ├── rules/
│   │   ├── testing.md                 ← paths: ["**/*.test.ts", "**/*.test.tsx"]
│   │   └── terraform.md               ← paths: ["terraform/**/*"]
│   ├── commands/
│   │   ├── review-pr.md               ← /review-pr
│   │   └── add-endpoint.md            ← /add-endpoint
│   ├── skills/
│   │   └── migrate-schema/
│   │       └── SKILL.md               ← context: fork, allowed-tools, argument-hint
│   └── CLAUDE.md          (or root)
├── .mcp.json                          ← committed, with ${ENV_VAR} references
└── docs/
    └── api-conventions.md             ← pulled in via @import

Requirements

  1. Root CLAUDE.md with genuinely always-relevant conventions, composed via @import ./docs/api-conventions.md.
  2. Two path-scoped rules files with paths: frontmatter. Verify the scoping works: open a test file and ask "what conventions apply here?", then open a .tf file and ask again. The answers must differ.
  3. Two slash commands in .claude/commands/. Commit them, then clone the repo to a second directory and confirm both commands are available there without any additional setup. This is the experiment that makes the project-vs-user distinction permanent.
  4. One skill with context: fork, allowed-tools, and argument-hint. Give it a procedure that does verbose exploration, and observe that the exploration doesn't appear in your main conversation.
  5. One user-level item — put a personal preference in ~/.claude/CLAUDE.md, then confirm it does not appear in the cloned repo's git status.
  6. .mcp.json with at least one server using ${ENV_VAR} expansion. Unset the variable and observe the failure; set it and observe success.
  7. Run /memory and confirm which files are loaded. Then deliberately misplace one (put a rules file at the repo root instead of .claude/rules/) and run /memory again to see it absent.

Success criteria

  • /memory output matches your intended hierarchy exactly.
  • Test conventions do not load when you're working on Terraform, and vice versa.
  • The fresh clone has your slash commands and MCP servers, and does not have your user-level preferences.
  • The skill's exploration output stays out of the main context.

🎯 The lesson to extract

Step 3 and step 5 together are the whole "team-wide vs personal" fork that Domain 3 tests four different ways. Doing it once physically — cloning and seeing what's there — is worth more than re-reading the table.


Lab 3 — Structured extraction pipeline

Maps to: Domains 4, 5 · Scenario 6 Exam payoff: syntax-vs-semantics, nullable fields, escape-hatch enums, retry-with-feedback, per-type validation, batching.

Build

Collect 30–50 real documents of at least three types — invoices, receipts, and something awkward like a bank statement or a credit note. Include some that are genuinely missing fields.

1. Define a schema as a tool input_schema, forced with tool_choice. Include:

  • a nullable field for something often absent (purchase_order)
  • an enum with "unclear" and "other" plus a *_detail string
  • stated_total, calculated_total, conflict_detected

2. Add format normalization rules to the prompt for dates and amounts, including the ambiguous-date rule.

3. Add semantic validation in your code: re-sum the line items yourself and compare. Don't trust the model's calculated_total.

4. Add retry with error feedback: document + failed extraction + specific errors, max 3 attempts, then route to a review queue.

5. Hand-label the ground truth for all 30–50 documents. Then build the per-type, per-field accuracy table.

6. Compute the aggregate accuracy too — and compare it to the worst cell in your table.

7. Run the same corpus through the Message Batches API with custom_id correlation. Introduce a deliberate failure in a few requests and practise resubmitting only those IDs.

Success criteria

  • Zero schema-invalid outputs (the forced tool call guarantees this).
  • At least one output that is schema-valid and semantically wrong — find it and look at it. This is the exam's central Domain 4 fact made concrete.
  • With purchase_order required and non-nullable: at least one fabricated value. With it nullable: none.
  • Your per-type/per-field table shows at least one cell substantially below the aggregate.
  • You can state which specific (type, field) combinations you'd automate and which you'd route to humans.

🎯 The lesson to extract

Step 6 is the point of the lab. When your aggregate says 94% and your statement-line_items cell says 58%, the "97% exceeds our threshold, proceed with automation" distractor becomes visibly absurd — which is exactly how the exam wants you to see it.


Lab 4 — Debug a multi-agent research pipeline

Maps to: Domains 1, 2, 5 · Scenario 3 Exam payoff: narrow decomposition, scope partitioning, parallel Task calls, context isolation, error propagation, provenance.

This lab is deliberately a debugging exercise, because that's how Domain 1 items are framed.

Build

A coordinator with three subagents: web-research, document-analysis, synthesis. Give it a broad research question with several genuinely distinct dimensions — e.g. "assess the competitive position of X," which should cover product, pricing, distribution, regulation, and financials.

Then introduce each bug, observe it, and fix it

Bug 1 — Missing Task tool. Omit "Task" from the coordinator's allowedTools. Observe that it silently tries to do everything itself. Fix: add it. This is Scenario 3 Archetype 4.

Bug 2 — Narrow decomposition. Prompt the coordinator vaguely ("research this topic and delegate as needed"). Log every subtask it creates. You will very likely see it cluster into one or two dimensions. Fix: explicit scope partitioning across named dimensions, plus a coverage-check-and-re-delegate loop. Archetype 1 — the highest-probability item in the scenario.

Bug 3 — No scope partitioning. Give three subagents the same broad instruction. Observe duplicated sources in the returns. Fix: disjoint scopes stated in each prompt, with "another agent is handling X." Archetype 2.

Bug 4 — Implicit context. Write a delegating prompt that refers to "the company we discussed" and "the timeframe mentioned earlier." Watch the subagent flounder — it has none of that. Fix: pass every identifier, path, constraint, and prior finding literally. This makes Reflex 5 permanent.

Bug 5 — Sequential delegation. Issue one Task per turn and time it. Fix: emit all independent Task calls in a single response and time it again. Archetype 3.

Bug 6 — Access failure as empty result. Make one document collection return [] with isError: false on a permission failure. Observe the final report confidently stating no internal documents exist. Fix: isError: true, errorCategory: "permission", isRetryable: false. Archetype 6 — and this is the bug most worth experiencing, because the failure is completely silent.

Bug 7 — Workflow termination. Make the coordinator abort when any subagent errors. Observe three successful streams thrown away. Fix: propagate structured errors with partial results and annotate the coverage gap in the report. Archetype 7.

Bug 8 — Lost provenance. Have the synthesis agent produce flowing prose from the collected claims. Try to verify a specific sentence against a source. Fix: structured claim→source records carried through synthesis, with publication and collection dates. Archetypes 8 and 9.

Success criteria

  • You have reproduced all eight bugs and seen each symptom yourself.
  • For Bug 2, you have a log showing narrow subtask clustering — the literal evidence the exam describes.
  • For Bug 6, you have a report that is confidently wrong with no error anywhere in the logs.
  • After all fixes: every claim in the final report traces to a dated source, and any inaccessible dimension is explicitly annotated.

🎯 The lesson to extract

Bug 6 is the one to sit with. A permission error became "there is no data," and nothing in the system noticed. That's why the exam treats "an access failure is not a valid empty result" as a first-order architectural fact rather than an implementation detail.


What to do if you have no time to build

If you have fewer than three days, skip the labs and do this instead — a 30-minute paper exercise per scenario:

For each of the six scenarios, on a blank page, write:

  1. The tool list (or config surface) from memory.
  2. Three ways it could fail.
  3. For each failure: what layer is broken, and what the smallest correct fix is.
  4. For each failure: which distractor would be most tempting.

Then check it against Chapter 13. Anything you couldn't produce is a study target. This exercise correlates better with exam performance than re-reading chapters does, because it rehearses the actual task: diagnose the layer, choose the smallest fix, reject the tempting wrong answer.