Message Types in the Claude Agent SDK
The system, assistant, and result message types that every agent run produces. What each contains, when it fires, and how to use them for session tracking, cost logging, and error detection.
Why message types matter
Every agent run produces a stream of messages. Up to this point, most examples filter for one specific message type and ignore the rest. That works for simple demos.
In production, you need more. You need to know when a session started and what session ID it received. You need to log tool usage. You need the cost and duration at the end of each run. You need to detect when a session failed versus succeeded.
All of that information is in the message stream. It is organized by type.
The three primary types
System messages
Fire once at the very start of each session. This is the session initialization message.
if (message.type === "system" && message.subtype === "init") {
console.log("Session ID:", message.session_id);
console.log("Model:", message.model);
console.log("Available tools:", message.tools);
console.log("Permission mode:", message.permission_mode);
}
The session ID here is what you need for conversation resumption. Capture it early in the message stream so you have it before the session ends.
Assistant messages
Fire throughout the session as the agent works. This is everything Claude generates: text blocks, tool calls, thinking blocks.
if (message.type === "assistant") {
for (const block of message.message.content) {
if (block.type === "text") {
console.log("Text:", block.text);
}
if (block.type === "tool_use") {
console.log("Tool called:", block.name);
console.log("Tool input:", block.input);
}
}
}
For streaming responses to a UI, write text blocks to the output as they arrive rather than waiting for the full result message. For audit logging, capture every tool call with its inputs.
Result messages
Fire once at the very end of each session. This is the session completion message.
if (message.type === "result") {
if (message.subtype === "success") {
console.log("Result:", message.result);
console.log("Session ID:", message.session_id);
console.log("Cost:", message.total_cost_usd);
console.log("Duration:", message.duration_ms, "ms");
console.log("Turns:", message.num_turns);
} else {
console.log("Failed with subtype:", message.subtype);
// Subtypes: "error", "max_turns", "max_budget_usd"
}
}
The result subtypes tell you why the session ended:
"success"— completed normally"error"— something went wrong"max_turns"— hit the turn limit"maxbudgetusd"— hit the spending cap
A complete message handler
for await (const message of query(messages(), options)) {
switch (message.type) {
case "system":
if (message.subtype === "init") {
sessionId = message.session_id;
}
break;
case "assistant":
for (const block of message.message.content) {
if (block.type === "text") {
process.stdout.write(block.text);
}
if (block.type === "tool_use") {
console.log(`\n[Tool: ${block.name}]`);
}
}
break;
case "result":
if (message.subtype === "success") {
finalResponse = message.result;
console.log(`\nCost: $${message.total_cost_usd}`);
console.log(`Turns: ${message.num_turns}`);
}
break;
}
}
Local development note on cost
The cost logged in result messages reflects what the session would cost with an API key. If you are developing with a Claude.ai subscription, you are not actually being charged that amount. The logging is still useful — it gives you a realistic picture of production costs before you deploy.
> For operators building billing into their products: The totalcostusd field in the result message is the mechanism for cost-based billing. If you are building a client-facing tool and want to charge based on usage, this is where you get the number. Combine it with a user identifier and you have per-user cost tracking.
---
Author: FractionalSkill