Anthropic just dropped Opus 4.8, and I went deep on what actually changed versus 4.7. Spoiler: the new "dynamic workflow" tool is more interesting than the version bump suggests. Here's my honest breakdown of whether it's worth switching
We use Claude for a few different things: an automated code review assistant, some internal document analysis pipelines, and a handful of one-off agent tasks that our DevOps lead keeps spinning up and then abandoning (he's working on it). Opus 4.7 is a genuinely good model. I want to say that clearly before getting into where 4.8 improves on it, because I've seen too many "new model is better" posts that treat the previous version like it was broken. It wasn't. On most of our workloads, 4.7 was fast, accurate, and cost-predictable. The problems showed up in specific places. Long multi-step reasoning tasks where the model needed to hold a lot of context and adapt based on intermediate findings. That's where 4.7 started to feel like it was working harder than it should have to, and where the outputs got inconsistent enough that we built extra validation layers just to catch drift. We're a Java shop for most of our backend services, so we'd been using Spring AI to wire Claude into our tooling. That context matters for everything below. The headline feature: dynamic workflows The biggest addition in 4.8 is a capability Anthropic is calling dynamic workflows. In 4.7, when you built an agent that used tools, the model was reactive. It picked the next tool, ran it, looked at the result, picked the next tool. The "plan" was implicit, a series of local decisions chained together. You, the developer, had to encode the branching logic in your orchestration layer. In 4.8, the model can construct and revise an explicit plan at runtime, based on what it discovers along the way. The plan is a first-class object you can inspect. When results contradict the model's assumptions, it updates the plan before continuing rather than blindly executing the next step. To make this concrete: we spent six weeks this past spring building a code review assistant on top of 4.7 using Spring AI's ChatClient and a custom tool-routing layer. By the end we had a deeply stateful service class I genuinely dreaded touching. Most of that complexity wasn't the model's reasoning. It was us trying to anticipate every path the reasoning might take. The 4.7 approach: you own the state machine Here's the full picture of how the 4.7 setup worked. The service itself was simple enough. The pain was everything around it: @Service public class CodeReviewService { private final ChatClient chatClient; public CodeReviewService(ChatClient.Builder builder) { this.chatClient = builder .defaultSystem(""" You are a code review agent. Use the available tools to review the PR. Call tools one at a time and report what you find. """) .build(); } public ReviewResult runReview(String prDescription) { List<Message> messages = new ArrayList<>(); messages.add(new UserMessage(prDescription)); ReviewState state = new ReviewState(); // We had to manually drive the loop because 4.7 wouldn't // reliably chain tool calls across turns without losing context. while (!state.isComplete()) { ChatResponse response = chatClient.prompt() .options(AnthropicChatOptions.builder() .model("claude-opus-4-7") .maxTokens(4096) .build()) .messages(messages) .tools(new ReadFileTool(), new SearchCodebaseTool(), new CheckDepsTool(), new RunTestsTool()) .call() .chatResponse(); AssistantMessage assistantMessage = response.getResult().getOutput(); messages.add(assistantMessage); // Inspect which tool was called and route accordingly. // This routing logic is where the 400 lines came from. List<ToolCall> toolCalls = assistantMessage.getToolCalls(); if (toolCalls.isEmpty()) { state.markComplete(); } else { for (ToolCall toolCall : toolCalls) { ToolResult result = dispatchTool(toolCall, state); messages.add(new ToolResultMessage(toolCall.id(), result.content())); updateStateFromResult(state, toolCall.name(), result); } } } return state.toReviewResult(); } private void updateStateFromResult(ReviewState state, String toolName, ToolResult result) { // This is the logic that grew arms and legs. // Every new edge case added another branch here. switch (toolName) { case "read_file" -> { state.markFileRead(result.filePath()); if (result.containsAuthCode()) { state.requiresDependencyExpansion(true); } } case "search_codebase" -> state.addDependencies(result.dependencies()); case "check_deps" -> { if (result.hasFirstPartyDeps()) { state.requiresFirstPartyCheck(true); } } case "run_tests" -> state.addTestResults(result.failures()); } // Decide whether to keep going based on accumulated state. // If we hadn't thought of a condition here, it just didn't happen. if (state.requiresDependencyExpansion() && !state.dependenciesChecked()) { state.addPendingAction("check_dependencies"); } if (state.requiresFirstPartyCheck() && !state.firstPartyChecked()) { state.addPendingAction("check_first_party_packages"); } if (state.pendingActions().isEmpty() && state.filesRead()) { state.markComplete(); } } } That updateStateFromResult method was the thing that kept growing. Every time we found a new edge case in production, someone added another condition. By the end it was the most brittle code in the repo. The 4.8 approach: the model owns the planning layer With 4.8, the whole loop collapses. The model tracks what it's discovered and decides what to do next. We just give it tools and a goal: @Service public class DynamicCodeReviewService { private final ChatClient chatClient; private static final String SYSTEM_PROMPT = """ You are a code review agent. When given a task, follow this process: 1. Produce an explicit investigation plan before calling any tools. Format it as: PLAN: [numbered list of steps] 2. Execute the plan step by step. 3. After each tool call, state what you expected vs what you found. 4. If findings change your assumptions, revise the plan before continuing. Format revisions as: REVISED PLAN: [updated numbered list] 5. Only conclude when you're confident the scope is fully understood. """; public DynamicCodeReviewService(ChatClient.Builder builder) { this.chatClient = builder .defaultSystem(SYSTEM_PROMPT) .build(); } public String reviewPullRequest(String prDescription) { // No state machine. No routing logic. No updateStateFromResult(). // The model drives its own investigation. return chatClient.prompt() .options(AnthropicChatOptions.builder() .model("claude-opus-4-8") .maxTokens(8096) .build()) .user(prDescription) .tools(new ReadFileTool(), new SearchCodebaseTool(), new CheckDepsTool(), new RunTestsTool()) .call() .content(); } } The service went from roughly 180 lines with the state machine to 45 and it catches more issues. A direct comparison on the same task I ran both models against the same test case to see exactly where the behavior diverged. The scenario: review a PR described as a "simple token refresh fix," but...