Skip to main content
Hooks let you register middleware callbacks that fire at key points in the agent execution lifecycle. Register once with WithHooks — the SDK calls your functions automatically across Run, RunAsync, and Stream. Every hook can inspect, mutate, or abort the operation it wraps. Return an error from any hook to abort the run immediately.

Register hooks

a, err := agent.NewAgent(
    agent.WithLLMClient(llmClient),
    agent.WithHooks("guardrails", agent.AgentHooks{
        BeforeLLM: []agent.BeforeLLMHook{
            func(ctx context.Context, in agent.BeforeLLMHookInput) (agent.BeforeLLMHookOutput, error) {
                // inspect or modify in.Request before the LLM call
                return agent.BeforeLLMHookOutput{Request: in.Request}, nil
            },
        },
        AfterLLM: []agent.AfterLLMHook{
            func(ctx context.Context, in agent.AfterLLMHookInput) (agent.AfterLLMHookOutput, error) {
                // inspect or modify in.Response after the LLM call
                return agent.AfterLLMHookOutput{Response: in.Response}, nil
            },
        },
    }),
)
Call WithHooks multiple times to register independent groups. Each group runs under its own name in RunMeta.HooksGroup:
a, err := agent.NewAgent(
    agent.WithHooks("pii-scrubber", piiHooks),
    agent.WithHooks("cost-tracker", costHooks),
    agent.WithHooks("audit-logger", auditHooks),
    // ...
)
Hook group names participate in the Temporal agent config fingerprint. The agent and worker processes must register the same hook group names — implementations are not hashed, only the names.

Hook points

HookFiresWhat you can mutate
BeforeLLMHookBefore each LLM requestRequest (messages, system prompt, tools, params)
AfterLLMHookAfter each LLM responseResponse (content, tool calls)
BeforeToolHookBefore native/MCP tool executesArgs
AfterToolHookAfter native/MCP tool executesContent, Err
BeforeRetrieveHookBefore each retriever searchQuery
AfterRetrieveHookAfter each retriever searchDocuments
BeforeMemoryLoadHookBefore memory recallQuery, Limit, MinScore, Kinds
AfterMemoryLoadHookAfter memory recall, before prompt injectionPromptContext
BeforeMemoryStoreHookBefore each memory record is persistedRecord, ID
AfterMemoryStoreHookAfter memory is persistedError/abort only
Every hook receives RunMeta with RunID, Iteration, and HooksGroup for correlation.

Use cases

Guardrails and safety

Block unsafe inputs before they reach the LLM and filter toxic outputs before they reach the user:
agent.WithHooks("safety", agent.AgentHooks{
    BeforeLLM: []agent.BeforeLLMHook{
        func(ctx context.Context, in agent.BeforeLLMHookInput) (agent.BeforeLLMHookOutput, error) {
            if containsInjection(in.Request.Messages) {
                return agent.BeforeLLMHookOutput{}, errors.New("prompt injection detected")
            }
            return agent.BeforeLLMHookOutput{Request: in.Request}, nil
        },
    },
    AfterLLM: []agent.AfterLLMHook{
        func(ctx context.Context, in agent.AfterLLMHookInput) (agent.AfterLLMHookOutput, error) {
            if isToxic(in.Response.Content) {
                return agent.AfterLLMHookOutput{}, errors.New("unsafe output blocked")
            }
            return agent.AfterLLMHookOutput{Response: in.Response}, nil
        },
    },
})
Hooks: BeforeLLM, AfterLLM

PII and data privacy

Scrub sensitive data before it leaves your system or gets persisted:
agent.WithHooks("pii", agent.AgentHooks{
    BeforeLLM: []agent.BeforeLLMHook{
        func(ctx context.Context, in agent.BeforeLLMHookInput) (agent.BeforeLLMHookOutput, error) {
            req := in.Request
            req.SystemMessage = redactPII(req.SystemMessage)
            return agent.BeforeLLMHookOutput{Request: req}, nil
        },
    },
    BeforeMemoryStore: []agent.BeforeMemoryStoreHook{
        func(ctx context.Context, in agent.BeforeMemoryStoreHookInput) (agent.BeforeMemoryStoreHookOutput, error) {
            rec := in.Record
            rec.Text = redactPII(rec.Text)
            return agent.BeforeMemoryStoreHookOutput{Record: rec}, nil
        },
    },
})
Hooks: BeforeLLM, AfterLLM, BeforeTool, AfterTool, BeforeRetrieve, AfterRetrieve, BeforeMemoryStore, AfterMemoryLoad

Token budget and cost tracking

Enforce a per-run or per-user token budget across LLM calls:
agent.WithHooks("cost", agent.AgentHooks{
    AfterLLM: []agent.AfterLLMHook{
        func(ctx context.Context, in agent.AfterLLMHookInput) (agent.AfterLLMHookOutput, error) {
            if in.Response.Usage != nil {
                trackTokens(in.RunMeta.RunID, in.Response.Usage.TotalTokens)
                if budgetExceeded(in.RunMeta.RunID) {
                    return agent.AfterLLMHookOutput{}, errors.New("token budget exceeded")
                }
            }
            return agent.AfterLLMHookOutput{Response: in.Response}, nil
        },
    },
})
Hooks: BeforeLLM (cache read / short-circuit), AfterLLM (token tracking, cache write, model fallback)

Tenant isolation for memory

Enforce that each tenant only reads and writes their own memories:
agent.WithHooks("tenant-isolation", agent.AgentHooks{
    BeforeMemoryLoad: []agent.BeforeMemoryLoadHook{
        func(ctx context.Context, in agent.BeforeMemoryLoadHookInput) (agent.BeforeMemoryLoadHookOutput, error) {
            if in.Scope.TenantID == "" {
                return agent.BeforeMemoryLoadHookOutput{}, errors.New("tenant ID required for memory access")
            }
            return agent.BeforeMemoryLoadHookOutput{
                Query: in.Query, Limit: in.Limit,
                MinScore: in.MinScore, Kinds: in.Kinds,
            }, nil
        },
    },
    BeforeMemoryStore: []agent.BeforeMemoryStoreHook{
        func(ctx context.Context, in agent.BeforeMemoryStoreHookInput) (agent.BeforeMemoryStoreHookOutput, error) {
            if in.Scope.TenantID == "" {
                return agent.BeforeMemoryStoreHookOutput{}, errors.New("tenant ID required for memory store")
            }
            return agent.BeforeMemoryStoreHookOutput{Record: in.Record, ID: in.ID}, nil
        },
    },
})
Hooks: BeforeMemoryLoad, BeforeMemoryStore

Audit logging

Log every operation for observability without touching business logic:
agent.WithHooks("audit", agent.AgentHooks{
    AfterLLM: []agent.AfterLLMHook{
        func(ctx context.Context, in agent.AfterLLMHookInput) (agent.AfterLLMHookOutput, error) {
            log.Printf("run=%s iter=%d tokens=%d",
                in.RunMeta.RunID, in.RunMeta.Iteration, in.Response.Usage.TotalTokens)
            return agent.AfterLLMHookOutput{Response: in.Response}, nil
        },
    },
    AfterTool: []agent.AfterToolHook{
        func(ctx context.Context, in agent.AfterToolHookInput) (agent.AfterToolHookOutput, error) {
            log.Printf("run=%s tool=%s err=%v", in.RunMeta.RunID, in.Call.Name, in.Err)
            return agent.AfterToolHookOutput{Content: in.Content, Err: in.Err}, nil
        },
    },
    AfterMemoryStore: []agent.AfterMemoryStoreHook{
        func(ctx context.Context, in agent.AfterMemoryStoreHookInput) (agent.AfterMemoryStoreHookOutput, error) {
            log.Printf("run=%s memory stored id=%s", in.RunMeta.RunID, in.ID)
            return agent.AfterMemoryStoreHookOutput{}, nil
        },
    },
})
Hooks: BeforeLLM, AfterLLM, BeforeTool, AfterTool, BeforeRetrieve, AfterRetrieve, AfterMemoryLoad, AfterMemoryStore

Retrieval query rewriting and re-ranking

Modify the query before search and filter or re-rank results after:
agent.WithHooks("retrieval", agent.AgentHooks{
    BeforeRetrieve: []agent.BeforeRetrieveHook{
        func(ctx context.Context, in agent.BeforeRetrieveHookInput) (agent.BeforeRetrieveHookOutput, error) {
            return agent.BeforeRetrieveHookOutput{Query: expandQuery(in.Query)}, nil
        },
    },
    AfterRetrieve: []agent.AfterRetrieveHook{
        func(ctx context.Context, in agent.AfterRetrieveHookInput) (agent.AfterRetrieveHookOutput, error) {
            return agent.AfterRetrieveHookOutput{Documents: rerank(in.Documents)}, nil
        },
    },
})
Hooks: BeforeRetrieve (query rewriting), AfterRetrieve (filtering, re-ranking)

Use case reference

GoalHooks to use
Prompt injection / input guardrailsBeforeLLM
Output content filteringAfterLLM
PII scrubbing from prompts / responsesBeforeLLM, AfterLLM
PII scrubbing from tool args / resultsBeforeTool, AfterTool
PII scrubbing from retrievalBeforeRetrieve, AfterRetrieve
PII scrubbing from memoryBeforeMemoryStore, AfterMemoryLoad
Token tracking and budget enforcementAfterLLM
LLM response cachingBeforeLLM (read), AfterLLM (write)
Model fallback on errorAfterLLM
Tool rate limitingBeforeTool
Tool input validation / authorizationBeforeTool
Log all operationsBeforeLLM, AfterLLM, BeforeTool, AfterTool, BeforeRetrieve, AfterRetrieve, AfterMemoryLoad, AfterMemoryStore
Retrieval query rewritingBeforeRetrieve
Document re-ranking / filteringAfterRetrieve
Memory query rewritingBeforeMemoryLoad
Filter injected memory contextAfterMemoryLoad
Memory tenant isolationBeforeMemoryLoad, BeforeMemoryStore
Control what gets persistedBeforeMemoryStore
Audit memory reads / writesAfterMemoryLoad, AfterMemoryStore

Example

Hooks

PII scrubbing, retrieval filtering, memory tenant isolation — every hook point registered

Memory

BeforeMemoryLoad and BeforeMemoryStore hooks

Retrieval

BeforeRetrieve and AfterRetrieve hooks

Observability

OTLP traces and metrics alongside hooks

Configuration

WithHooks reference