TypeScriptADK-TS

Callback Types

Signatures, trigger points, and return behavior for all six ADK-TS callback types — agent, model, and tool.

ADK-TS provides six callback types across three lifecycle stages: agent execution, LLM interaction, and tool execution. This page covers the exact signature for each, when it fires, and what each return value does.

Agent lifecycle callbacks

These fire around the agent's own execution and are available on all agent types — LlmAgent, SequentialAgent, ParallelAgent, LoopAgent, and any custom agent extending BaseAgent.

beforeAgentCallback

Fires immediately before the agent's runAsyncImpl begins. Use it to validate state, initialise resources, log entry points, or skip the agent entirely by returning early.

import type { Content } from "@google/genai";
import type { CallbackContext } from "@iqai/adk";

type BeforeAgentCallback = (
  callbackContext: CallbackContext,
) => Promise<Content | undefined> | Content | undefined;

Returning undefined lets the agent run normally. Returning a Content object skips the agent entirely and uses that content as its output.

import { LlmAgent } from "@iqai/adk";
import type { CallbackContext } from "@iqai/adk";
import type { Content } from "@google/genai";

const agent = new LlmAgent({
  name: "guarded_agent",
  model: "gemini-2.5-flash",
  instruction: "You are a helpful assistant.",
  description: "An agent with a guardrail on availability.",
  beforeAgentCallback: (
    callbackContext: CallbackContext,
  ): Content | undefined => {
    const isBlocked = callbackContext.state.get("agent_blocked");

    if (isBlocked) {
      return {
        role: "model",
        parts: [{ text: "This agent is currently unavailable." }],
      };
    }

    console.log(`Agent started: ${callbackContext.agentName}`);
    return undefined;
  },
});

afterAgentCallback

Fires immediately after runAsyncImpl completes successfully. Does not fire if the agent was skipped by beforeAgentCallback. Use it for cleanup, post-processing, or replacing the agent's final output.

import type { Content } from "@google/genai";
import type { CallbackContext } from "@iqai/adk";

type AfterAgentCallback = (
  callbackContext: CallbackContext,
) => Promise<Content | undefined> | Content | undefined;

Returning undefined passes through the agent's original output. Returning a Content object replaces it.

import type { CallbackContext } from "@iqai/adk";
import type { Content } from "@google/genai";

const afterAgentCallback = (
  callbackContext: CallbackContext,
): Content | undefined => {
  const count = (callbackContext.state.get("run_count") ?? 0) + 1;
  callbackContext.state.set("run_count", count);
  console.log(`Agent ${callbackContext.agentName} completed (run #${count})`);
  return undefined;
};

LLM interaction callbacks

These are specific to LlmAgent and fire around each call to the underlying language model. A single agent turn may involve multiple LLM calls, so these can fire more than once per invocation.

beforeModelCallback

Fires just before the LLM request is sent. Use it to inspect or modify the request — inject dynamic instructions, add few-shot examples, enforce content policies, or return a cached response to skip the network call entirely.

import type { LlmRequest, LlmResponse, CallbackContext } from "@iqai/adk";

type BeforeModelCallback = (args: {
  callbackContext: CallbackContext;
  llmRequest: LlmRequest;
}) => Promise<LlmResponse | null | undefined> | LlmResponse | null | undefined;

Returning null or undefined lets the LLM call proceed (with any in-place modifications you made to llmRequest). Returning an LlmResponse skips the LLM call and uses that response directly.

import { LlmAgent, LlmResponse } from "@iqai/adk";
import type { CallbackContext, LlmRequest } from "@iqai/adk";

const agent = new LlmAgent({
  name: "policy_agent",
  model: "gemini-2.5-flash",
  instruction: "You are a helpful assistant.",
  description: "An agent with a content policy on certain topics.",
  beforeModelCallback: ({
    callbackContext,
    llmRequest,
  }: {
    callbackContext: CallbackContext;
    llmRequest: LlmRequest;
  }): LlmResponse | null => {
    const lastMessage =
      llmRequest.contents[llmRequest.contents.length - 1]?.parts?.[0]?.text ??
      "";

    if (lastMessage.toLowerCase().includes("restricted")) {
      return new LlmResponse({
        content: {
          role: "model",
          parts: [{ text: "I cannot help with that topic." }],
        },
      });
    }

    // Inject the user's preferred language into the system instruction
    const lang = callbackContext.state.get("preferred_language");
    if (lang && llmRequest.config) {
      const current = llmRequest.getSystemInstructionText() ?? "";
      llmRequest.config.systemInstruction =
        `${current}\nRespond in: ${lang}`.trim();
    }

    return null;
  },
});

afterModelCallback

Fires after a response is received from the LLM, before it's processed further. Use it to log model outputs, extract structured data into state, censor sensitive content, or reformat the response.

import type { LlmResponse, CallbackContext } from "@iqai/adk";

type AfterModelCallback = (args: {
  callbackContext: CallbackContext;
  llmResponse: LlmResponse;
}) => Promise<LlmResponse | null | undefined> | LlmResponse | null | undefined;

Returning null or undefined passes the original response through. Returning a new LlmResponse replaces it.

import type { CallbackContext, LlmResponse } from "@iqai/adk";

const afterModelCallback = ({
  callbackContext,
  llmResponse,
}: {
  callbackContext: CallbackContext;
  llmResponse: LlmResponse;
}): LlmResponse | null => {
  const text = llmResponse.content?.parts?.[0]?.text ?? "";

  // Pull out any JSON block and store it in state for downstream tools
  const match = text.match(/```json\n([\s\S]*?)\n```/);
  if (match) {
    try {
      callbackContext.state.set("extracted_json", JSON.parse(match[1]));
    } catch {
      // Malformed JSON — ignore
    }
  }

  return null;
};

Tool execution callbacks

These are specific to LlmAgent and fire around each tool call the LLM requests. They receive a ToolContext which extends CallbackContext with tool-specific methods like listArtifacts and searchMemory.

beforeToolCallback

Fires just before a tool's runAsync is invoked. Use it to validate arguments, check authorization, inject auth tokens, or return a cached result to skip execution.

import type { BaseTool, ToolContext } from "@iqai/adk";

type BeforeToolCallback = (
  tool: BaseTool,
  args: Record<string, any>,
  toolContext: ToolContext,
) =>
  | Promise<Record<string, any> | null | undefined>
  | Record<string, any>
  | null
  | undefined;

Returning null or undefined lets the tool run (with any in-place changes to args). Returning a Record<string, any> skips execution and uses it as the tool result.

import type { BaseTool, ToolContext } from "@iqai/adk";

const beforeToolCallback = (
  tool: BaseTool,
  args: Record<string, any>,
  toolContext: ToolContext,
): Record<string, any> | null => {
  const token = toolContext.state.get("api_token");

  if (!token) {
    return { error: "Authentication required. Please sign in first." };
  }

  // Inject the token so the tool can use it
  args.authToken = token;

  console.log(
    `Running tool: ${tool.name} (call ID: ${toolContext.functionCallId})`,
  );
  return null;
};

afterToolCallback

Fires after the tool's runAsync completes. Use it to log results, save important values into state, filter sensitive fields, or reformat the output before the LLM sees it.

import type { BaseTool, ToolContext } from "@iqai/adk";

type AfterToolCallback = (
  tool: BaseTool,
  args: Record<string, any>,
  toolContext: ToolContext,
  toolResponse: Record<string, any>,
) =>
  | Promise<Record<string, any> | null | undefined>
  | Record<string, any>
  | null
  | undefined;

Returning null or undefined passes the original result through. Returning a new object replaces it.

import type { BaseTool, ToolContext } from "@iqai/adk";

const afterToolCallback = (
  tool: BaseTool,
  args: Record<string, any>,
  toolContext: ToolContext,
  toolResponse: Record<string, any>,
): Record<string, any> | null => {
  // Persist the transaction ID for later reference
  if (toolResponse.transaction_id) {
    toolContext.state.set("last_transaction_id", toolResponse.transaction_id);
  }

  // Strip API keys from the result before the LLM sees them
  if (toolResponse.api_key) {
    const { api_key, ...safe } = toolResponse;
    return safe;
  }

  return null;
};

Callback arrays

Every callback property accepts either a single function or an array. When you pass an array, the framework calls each function in order and stops as soon as one returns a non-sentinel value.

import { LlmAgent } from "@iqai/adk";
import type { CallbackContext } from "@iqai/adk";
import type { Content } from "@google/genai";

const loggingCallback = (ctx: CallbackContext): Content | undefined => {
  console.log(`[log] Agent started: ${ctx.agentName}`);
  return undefined;
};

const guardCallback = (ctx: CallbackContext): Content | undefined => {
  if (ctx.state.get("maintenance_mode")) {
    return { role: "model", parts: [{ text: "Under maintenance." }] };
  }
  return undefined;
};

const agent = new LlmAgent({
  name: "multi_callback_agent",
  model: "gemini-2.5-flash",
  instruction: "You are a helpful assistant.",
  description: "An agent that demonstrates multiple beforeAgentCallbacks.",
  // loggingCallback always runs; guardCallback only blocks if maintenance_mode is set
  beforeAgentCallback: [loggingCallback, guardCallback],
});

Single callback types

Each callback type has a corresponding Single* variant that represents one function (before wrapping in an array). These are useful when defining a callback separately and you want TypeScript to infer the exact signature without accepting arrays:

Array typeSingle type
BeforeAgentCallbackSingleAgentCallback
AfterAgentCallbackSingleAgentCallback
BeforeModelCallbackSingleBeforeModelCallback
AfterModelCallbackSingleAfterModelCallback
BeforeToolCallbackSingleBeforeToolCallback
AfterToolCallbackSingleAfterToolCallback
import type {
  SingleBeforeModelCallback,
  SingleBeforeToolCallback,
} from "@iqai/adk";

// Explicitly typed — TypeScript enforces the exact function signature
const addUserContext: SingleBeforeModelCallback = ({
  callbackContext,
  llmRequest,
}) => {
  const lang = callbackContext.state.get("language") ?? "en";
  if (llmRequest.config) {
    llmRequest.config.systemInstruction =
      `${llmRequest.config.systemInstruction ?? ""}\nLanguage: ${lang}`.trim();
  }
  return null;
};

const checkAuth: SingleBeforeToolCallback = (tool, args, toolContext) => {
  if (!toolContext.state.get("api_token")) {
    return { error: "Not authenticated." };
  }
  return null;
};

CallbackContext and ToolContext provide state, artifact, and memory access. See CallbackContext and ToolContext for the full property and method reference.

Next steps