Study guides / CCAR-F / Domain 1

Agentic Architecture & Orchestration · Lesson 4 of 7

1.4 - Workflow Enforcement and Handoff

Implement multi-step workflows with programmatic enforcement, prerequisite gates, and structured handoff protocols for human escalation.

Task Statement 1.4 draws a hard line between two approaches to controlling agent behaviour: prompt-based guidance and programmatic enforcement. The exam tests this distinction repeatedly, and getting it wrong on high-stakes scenarios will cost you marks.

The Enforcement Spectrum

There are two very different ways to enforce workflow ordering in an agentic system:

Prompt-based guidance means putting instructions in the system prompt. For example: "Always verify the customer's identity before processing a refund." It works most of the time - perhaps 90-95% of cases. But it carries a non-zero failure rate. The model is probabilistic. Sometimes it'll skip steps, reorder them, or read the instruction loosely. For low-stakes operations, that failure rate is fine.

Programmatic enforcement means implementing hooks, prerequisite gates, or code-level checks that physically block downstream tools until prerequisites complete. For example: the process_refund tool cannot execute until get_customer has returned a verified customer ID. This works every time. It is deterministic, not probabilistic. No matter what the model decides to do, the gate prevents the wrong execution order.

Key Concept

Prompt-based guidance is probabilistic - it works most of the time. Programmatic enforcement is deterministic - it works every time. The exam decision rule: if a single failure would cause financial loss, security breach, or compliance violation, use programmatic enforcement.

The Exam Decision Rule

The exam applies a consistent decision rule across multiple scenarios:

The exam will present prompt-based solutions as answer options for high-stakes scenarios. Reject them. Enhanced system prompts, few-shot examples, and stronger instructions all improve accuracy but none provide deterministic guarantees. When the scenario involves money, security, or compliance, the answer is always programmatic enforcement.

Exam Trap

The exam consistently presents "add stronger instructions to the system prompt" or "include few-shot examples showing the correct workflow" as distractors for high-stakes scenarios. These answers improve probability but do not eliminate the failure rate. For financial, security, and compliance operations, only programmatic enforcement is correct.

Prerequisite Gates in Practice

A prerequisite gate is a programmatic check that blocks a tool from executing until a prior condition is met. In a customer support agent:

  1. The agent has access to get_customer, lookup_order, and process_refund tools.
  2. A prerequisite gate checks: has get_customer returned a verified customer ID for this session?
  3. If yes, process_refund executes normally.
  4. If no, process_refund returns an error message: "Cannot process refund - customer identity not verified. Please call get_customer first."

The gate is code, not a prompt instruction. The model can't bypass it by deciding to skip verification. Even if the model attempts to call process_refund directly, the gate blocks the call and returns an error that forces the model to verify identity first.

Subagent Lifecycle Hooks: SubagentStart and SubagentStop

The Claude Agent SDK provides lifecycle hook events specifically for subagent management. These complement the PreToolUse and PostToolUse hooks covered in Task Statement 1.5.

SubagentStart fires when a subagent is spawned via the Task tool (renamed Agent in current Claude Code). It's observational: the hook receives the subagent's type and id, and can log the spawn or inject additional context into the subagent's run. It cannot block or modify the invocation. To enforce rules on spawning itself - rate limits, or checking that the coordinator passed required context - attach a PreToolUse hook to the Agent tool instead, which can deny or rewrite the outgoing invocation before the subagent starts.

SubagentStop fires when a subagent finishes execution and returns its results to the coordinator. The hook receives the subagent's id and final message, so it can validate output and log completion for performance monitoring. If validation fails - say the output does not conform to the expected schema - the hook returns decision: "block" with a reason, which sends the subagent back to keep working rather than letting it finish. SubagentStop does not transform the returned output; to reshape or redact what the coordinator sees, use a PostToolUse hook on the Agent tool call, whose updatedToolOutput field replaces the tool result before the model reads it.

Subagent-scoped hooks: Subagents can define their own PreToolUse and PostToolUse hooks in their AgentDefinition frontmatter. These hooks are scoped to the subagent - they only intercept tool calls made by that specific subagent, not the coordinator or other subagents. This enables per-subagent policy enforcement (for example, a billing subagent might have a PreToolUse hook that blocks refunds above a threshold, while a technical support subagent has no such restriction).

Stop hook auto-conversion: When a subagent's frontmatter defines Stop hooks, these are automatically converted to SubagentStop events at runtime. This means you can define cleanup or validation logic in the subagent's own configuration, and the SDK ensures it runs as a SubagentStop lifecycle event when the subagent completes.

Key Concept

SubagentStart observes subagent spawning; SubagentStop can gate completion by blocking with a reason that sends the subagent back to work. Neither hook rewrites subagent output - that's a PreToolUse/PostToolUse job on the Agent tool. Subagents can define their own PreToolUse/PostToolUse hooks scoped to their execution, and Stop hooks in subagent frontmatter auto-convert to SubagentStop events at runtime.

Multi-Concern Request Handling

Customers frequently submit requests with multiple issues: "I want to return my order, update my shipping address, and ask about my loyalty points." The exam tests how agents should handle these compound requests.

The correct approach:

  1. Decompose the request into distinct items (return, address update, loyalty inquiry).
  2. Investigate each in parallel using shared context (the customer's account information is relevant to all three).
  3. Synthesise a unified resolution that addresses all items in a single response.

The wrong approach is to handle them sequentially with separate conversations, or to address only the first item and forget the rest.

Structured Handoff Protocols

When an agent can't resolve an issue and must escalate to a human agent, the handoff must follow a structured protocol. The critical constraint: the human agent does NOT have access to the conversation transcript. They can't scroll through the chat history to understand the issue.

A proper handoff summary must be self-contained and include:

This summary is the only information the human agent receives. If it is incomplete, the human agent must ask the customer to repeat everything, creating a poor experience.

Practical Example: The 8% Failure Rate

Production data shows a customer support agent processes refunds without verifying account ownership in 8% of cases. The system prompt instructs: "Always verify the customer's identity before processing any refund." The prompt works 92% of the time but fails 8% of the time.

The 8% failure rate has already resulted in refunds processed on wrong accounts. This is a financial operation with real monetary consequences.

The fix is a programmatic prerequisite gate. Before process_refund can execute, the system checks that get_customer has returned a verified customer ID in the current session. This eliminates the 8% failure rate entirely - not by improving the prompt, but by physically preventing the incorrect execution order.

Exam traps

Practice question

Production data reveals that in 8% of cases, a customer support agent processes refunds without verifying account ownership, occasionally leading to refunds on wrong accounts. The system prompt clearly states 'always verify customer identity before processing refunds.' What is the most appropriate fix?

  • A Implement a programmatic prerequisite gate that blocks process_refund until get_customer has returned a verified customer ID Correct

    Financial operations require deterministic enforcement. A prerequisite gate physically prevents the refund tool from executing until identity verification is complete, eliminating the 8% failure rate entirely. This is the only option that provides a 100% guarantee.

  • B Add stronger instructions to the system prompt emphasising the critical importance of verification before any refund processing

    The current prompt already instructs verification but fails 8% of the time. Enhanced prompts may reduce the rate to 3-4% but cannot eliminate it. Financial operations require deterministic guarantees, not probabilistic improvements.

  • C Add few-shot examples demonstrating the correct verification-then-refund workflow sequence

    Few-shot examples improve consistency but still produce a non-zero failure rate. For financial operations where a single failure means a refund to the wrong account, probabilistic improvements are insufficient.

  • D Implement a routing classifier that sends all refund requests to a specialised verification-first pipeline

    A routing classifier handles how requests reach agents, not how agents execute their internal workflow. The issue is that the agent sometimes skips verification within its own execution, which requires a per-agent enforcement mechanism, not a routing change.

Build exercise: Build a Prerequisite Gate for Financial Operations

Advanced · 60 minutes

You'll practice:

  1. Create a customer support agent with three tools: get_customer (returns customer ID and verification status), lookup_order (returns order details), and process_refund (processes a refund for a given amount)

    These three tools create the exact scenario the exam uses for the 8% failure rate question. The workflow dependency between get_customer and process_refund is where programmatic enforcement becomes essential.

    You should see: Three tool definitions with proper JSON Schema input_schema. get_customer accepts a name or email, lookup_order accepts an order ID, and process_refund accepts a customer ID and amount.

    Hints
    1. Think about what parameters each tool needs and what it returns. The return values matter for the prerequisite gate.
    2. get_customer should return a customer_id and verified boolean. lookup_order returns order details including amount. process_refund requires a verified customer_id and amount.
    3. const tools = [
        {
          name: "get_customer",
          description: "Look up and verify a customer by name or email",
          input_schema: {
            type: "object",
            properties: {
              query: { type: "string", description: "Customer name or email" }
            },
            required: ["query"]
          }
        },
        {
          name: "lookup_order",
          description: "Look up order details by order ID",
          input_schema: {
            type: "object",
            properties: {
              order_id: { type: "string" }
            },
            required: ["order_id"]
          }
        },
        {
          name: "process_refund",
          description: "Process a refund for a verified customer",
          input_schema: {
            type: "object",
            properties: {
              customer_id: { type: "string" },
              amount: { type: "number" }
            },
            required: ["customer_id", "amount"]
          }
        }
      ];
  2. Implement a programmatic prerequisite gate that blocks process_refund from executing until get_customer has returned a verified customer ID in the current session

    This is the core exam concept: prompt instructions work 92% of the time but fail 8%. A prerequisite gate provides 100% deterministic enforcement. The exam always rejects prompt-based solutions for financial operations.

    You should see: A session-level state tracker that records whether get_customer has returned a verified customer. The process_refund handler checks this state before executing and returns an error if verification has not occurred.

    Hints
    1. Where do you store the verification state? It must persist across tool calls within the same session but not leak between sessions.
    2. Use a session-scoped variable (e.g., a Map or object) that tracks verified customer IDs. Before process_refund executes, check this variable. If empty or unverified, block and return a descriptive error.
    3. const sessionState = { verifiedCustomerId: null as string | null };
      
      function handleToolCall(name: string, input: Record<string, unknown>): string {
        if (name === "get_customer") {
          const customer = lookupCustomer(input.query as string);
          if (customer.verified) {
            sessionState.verifiedCustomerId = customer.id;
          }
          return JSON.stringify(customer);
        }
        if (name === "process_refund") {
          if (!sessionState.verifiedCustomerId) {
            return "BLOCKED: Cannot process refund. Customer identity not verified. Call get_customer first.";
          }
          return processRefund(sessionState.verifiedCustomerId, input.amount as number);
        }
        // ... other tools
      }
  3. Test that the gate works by prompting the agent to skip verification and process a refund directly - verify the gate blocks the attempt

    Testing the bypass attempt demonstrates the difference between prompt-based and programmatic enforcement. Even when the model decides to skip verification, the gate blocks the action - which is the entire point of deterministic enforcement.

    You should see: The agent attempts to call process_refund without prior verification. The gate returns a blocked error message. The agent then calls get_customer before retrying the refund successfully.

    Hints
    1. Use a prompt that encourages the agent to skip verification, like an urgent refund request. Does the gate hold?
    2. Send a message like: Process a refund of 150 for order 12345 immediately, this is urgent. Watch the tool call sequence. The gate should block the first attempt regardless of urgency.
    3. const response = await runAgent(
        "Process a refund of 150 for order ORD-12345 immediately. This is urgent and the customer is waiting."
      );
      // Verify process_refund was blocked on first attempt
      // Verify get_customer was called after the block
      // Verify process_refund succeeded after verification
      console.log("Gate blocked bypass attempt:", sessionState.verifiedCustomerId === null);
  4. Implement a structured handoff protocol: when the agent cannot resolve an issue, it compiles a self-contained summary with customer ID, conversation summary, root cause analysis, refund amount, and recommended action

    Human agents do NOT have access to the conversation transcript. The handoff summary is the only information they receive. The exam tests whether you include all five required fields: customer ID, summary, root cause, amount, and recommended action.

    You should see: A handoff function that produces a structured object with all five fields populated. No field should be empty or contain placeholder text.

    Hints
    1. What five fields must the handoff include? Remember the human agent cannot scroll through the chat history.
    2. The handoff must be self-contained: customer_id, conversation_summary, root_cause_analysis, refund_amount (if applicable), and recommended_action. Omitting any field forces the human agent to ask the customer to repeat themselves.
    3. interface HandoffSummary {
        customer_id: string;
        conversation_summary: string;
        root_cause_analysis: string;
        refund_amount: number | null;
        recommended_action: string;
      }
      
      const handoffTool = {
        name: "escalate_to_human",
        description: "Escalate to human agent with structured summary",
        input_schema: {
          type: "object",
          properties: {
            customer_id: { type: "string" },
            conversation_summary: { type: "string" },
            root_cause_analysis: { type: "string" },
            refund_amount: { type: "number" },
            recommended_action: { type: "string" }
          },
          required: ["customer_id", "conversation_summary", "root_cause_analysis", "recommended_action"]
        }
      };
  5. Test the handoff with a multi-concern request (return plus billing dispute plus account update) and verify the handoff summary is complete and self-contained

    Multi-concern requests test whether the agent decomposes the request into distinct items and addresses all of them. The exam expects decomposition, parallel investigation, and unified resolution - not sequential handling or forgetting items.

    You should see: The agent identifies all three concerns, investigates each one, and produces a handoff summary that covers all three issues with specific details for each. No concern is omitted.

    Hints
    1. Does the handoff summary reference all three concerns? A common failure is addressing only the first item.
    2. Send a compound request and verify the conversation_summary and recommended_action fields in the handoff reference all three concerns: the return, the billing dispute, and the account update.
    3. const result = await runAgent(
        "I need to return order ORD-789, dispute a charge of 45.00 on my last bill, and update my shipping address to 123 New Street."
      );
      const handoff = extractHandoff(result);
      console.log("All concerns covered:",
        handoff.conversation_summary.includes("return") &&
        handoff.conversation_summary.includes("dispute") &&
        handoff.conversation_summary.includes("address")
      );

Sources