DEV Community

Cover image for Google ADK Callbacks Are a Policy Plane, Not Just Hooks
Raju Dandigam
Raju Dandigam

Posted on

Google ADK Callbacks Are a Policy Plane, Not Just Hooks

An agent proposes refund_order. The tool exists, its arguments are valid JSON, and the model sounds confident. None of that answers the question that matters: is this action allowed now?

Google's Agent Development Kit gives us interception points before and after agents, models, and tools. The tempting implementation is to add a log statement to each callback. The more useful design is to treat those callbacks as a small policy plane.

That does not mean placing every business rule in a callback. It means using the boundary to make four decisions explicit:

  • may this operation proceed?
  • should its inputs be normalized?
  • which budget or authorization applies?
  • what evidence should be recorded?

A callback can change control flow

ADK callbacks are not passive listeners. A beforeModelCallback can return a response and skip the model call. A beforeToolCallback can return a tool result and skip the tool. After-callbacks can replace results. That makes callback return values part of application behavior, not merely observability. The ADK callback documentation explains these interception semantics for TypeScript and the other supported SDKs.

Callback order is part of that behavior. ADK can run callback lists and plugin callbacks in sequence, with earlier callbacks able to modify inputs seen by later ones or short-circuit the remaining chain. Treat the registered order as reviewed configuration: authentication and tenant resolution should not accidentally run after a callback that can approve or execute an operation.

Start with a small decision vocabulary:

type PolicyDecision = {
  outcome: "allow" | "block";
  reasonCode: string;
  policyVersion: string;
};

function authorizeTool(
  toolName: string,
  args: Record<string, unknown>,
): PolicyDecision {
  if (toolName === "refund_order" && args.approved !== true) {
    return {
      outcome: "block",
      reasonCode: "APPROVAL_REQUIRED",
      policyVersion: "refund-policy-3",
    };
  }

  return {
    outcome: "allow",
    reasonCode: "POLICY_SATISFIED",
    policyVersion: "refund-policy-3",
  };
}
Enter fullscreen mode Exit fullscreen mode

This decision is deterministic and reviewable. It does not ask the model whether its own proposed action is safe.

Keep the callback thin

The callback should delegate to policy code rather than growing into an untestable collection of if statements. The following is deliberately schematic because exact tool callback types can change between ADK releases:

async function beforeTool({ tool, args }: {
  tool: { name: string };
  args: Record<string, unknown>;
}) {
  const decision = authorizeTool(tool.name, args);

  await policyEvidence.write({
    tool: tool.name,
    outcome: decision.outcome,
    reasonCode: decision.reasonCode,
    policyVersion: decision.policyVersion,
  });

  if (decision.outcome === "block") {
    return {
      error: "Tool execution blocked by policy",
      reasonCode: decision.reasonCode,
    };
  }

  return undefined; // continue with normal tool execution
}
Enter fullscreen mode Exit fullscreen mode

Register that function through the agent's beforeToolCallback. Use beforeModelCallback for model-input limits or serving an approved cache entry, and afterToolCallback for result normalization and outcome evidence.

The boundary becomes easier to review when responsibilities stay distinct:

Boundary Suitable responsibility
Before model Input limits, budget checks, approved cache lookup
Before tool Authorization, argument validation, freshness checks
After tool Result normalization, outcome classification
After agent Final evidence and response-level validation

Test absence, not only the error message

A guardrail test is incomplete if it checks only the returned text. The callback might return “blocked” after the real tool already ran.

it("does not execute a refund without approval", async () => {
  const refund = vi.fn();
  const decision = authorizeTool("refund_order", {
    orderId: "synthetic-123",
    approved: false,
  });

  if (decision.outcome === "allow") await refund();

  expect(decision.reasonCode).toBe("APPROVAL_REQUIRED");
  expect(refund).not.toHaveBeenCalled();
});
Enter fullscreen mode Exit fullscreen mode

At integration level, preserve evidence that the policy callback ran and the protected tool did not. That is stronger than matching a model-generated refusal sentence.

Also test the installed callback chain, not only the pure policy function. A unit test can prove authorizeTool() works while one runner quietly omits the plugin. A useful integration assertion records the callback name, policy version, decision, proposal ID, and whether execution began. For a blocked proposal, tool.execution.started must be absent.

Callbacks are not the entire security architecture

A callback runs inside the application process. It can contain a bug, be omitted from another agent, or be registered in the wrong order. High-risk authorization should also be enforced at the tool or service boundary. The application still needs least-privilege credentials, idempotency, audit records, and tests.

Google explicitly recommends ADK Plugins for modular security guardrails instead of scattering individual callbacks across agents. That is the right graduation path when the same policy must apply consistently to several agents.

Plugins still run inside your application. They improve reuse and consistency; they do not replace authorization at the resource server. Think of the callback or plugin as the orchestration decision point and the downstream service as the final enforcement point.

Make policy visible

Callbacks are most valuable when they expose a control point that already exists conceptually. Give each decision a stable reason code and policy version. Record bounded facts, not raw prompts or hidden reasoning. Test both the allowed path and the absence of a forbidden side effect.

Used that way, an ADK callback is more than a hook. It is where an agent proposal becomes an application decision.

References

Top comments (0)