Introducing Memory Hooks

Monitor your agent's memory cycle, or intercept what it sees and stores.

Patrycja Jenkner-SarPatrycja Jenkner-Sar·

Sep 29, 2026

·

4 min read

You can now add hooks to Mastra's memory system, letting you watch observations and reflections, or change what the Observer and Reflector see and store.

There are two types of hooks:

  • Lifecycle hooks (onObservationStart, onObservationEnd, onReflectionStart, onReflectionEnd): These let you report on a cycle externally, e.g. catch a cost spike by logging token usage per cycle, or trace why an agent has forgotten something.
  • Transform hooks (beforeObservation, afterObservation, beforeReflection, afterReflection): These let you intercept and modify the actual memory data. You can pare/remove noisy tool output, internal reasoning or redact secrets and PII.

Before hooks, observational memory didn't give levers to affect what happened during an observation or reflection cycle (other than filtering messages before they reached memory). Now the hooks sit inside the cycle.

We also shipped skillResultRedactor(), a ready-made version of a transform hook. When an agent uses a Skill, the tool result contains the skill's entire file contents. The Observer sees it every time. To avoid that, the redactor replaces the content with a placeholder. The agent still remembers the skill.

Get started

Install @mastra/memory and a storage adapter. The storage adapter is required for Observational Memory. Set it up either on the Memory instance or the main Mastra instance:

GNU BashTerminal
npm install @mastra/memory @mastra/libsql
note
Requires @mastra/memory@1.31.0 or later, added in PR #23167 and PR #24220.

Create the agent

Create an agent and configure memory:

TypeScriptsrc/mastra/agents/trip-planner-agent.ts
import { Agent } from "@mastra/core/agent";
import { Memory } from "@mastra/memory";
import { LibSQLStore } from "@mastra/libsql";
 
export const tripPlannerAgent = new Agent({
  // ...
  memory: new Memory({
    storage: new LibSQLStore({
      id: "memory-storage",
      url: "file:./memory.db"
    }),
    options: {
      observationalMemory: {
        model: "google/gemini-2.5-flash"
      }
    }
  })
});

Lifecycle hooks

Define lifecycle hooks. They can fire at the start and end of every cycle. This example logs when a cycle starts, and what it costs when it ends:

new Memory({
  // ...
  options: {
    observationalMemory: {
      model: "google/gemini-2.5-flash",
      hooks: {
        onObservationStart: (info) => {
          console.log(`[onObservationStart] ${info?.threadId} | ${info?.trigger}`);
        },
        onObservationEnd: ({ threadId, usage, error }) => {
          if (error) {
            console.error(`[onObservationEnd] ${threadId} | ${error}`);
          } else {
            console.log(`[onObservationEnd] ${threadId} | ${usage?.inputTokens} input tokens`);
          }
        },
        onReflectionStart: (info) => {
          console.log(`[onReflectionStart] ${info?.threadId} | ${info?.trigger}`);
        },
        onReflectionEnd: ({ threadId, usage, error }) => {
          if (error) {
            console.error(`[onReflectionEnd] ${threadId} | ${error}`);
          } else {
            console.log(`[onReflectionEnd] ${threadId} | ${usage?.inputTokens} input tokens`);
          }
        },
      },
    },
  },
})

Transform hooks

Define transform hooks. This example strips a bulky tool result before the Observer ever sees it:

const SIZE_THRESHOLD = 2000; // chars
 
new Memory({
  // ...
  options: {
    observationalMemory: {
      model: "google/gemini-2.5-flash",
      hooks: {
        beforeObservation: ({ messages }) => ({
          messages: messages.map((message) => ({
            ...message,
            content: {
              ...message.content,
              parts: message.content.parts.map((part) => {
                if (part.type !== "tool-invocation") return part;
                const resultStr = JSON.stringify(part.toolInvocation.result);
                if (resultStr.length <= SIZE_THRESHOLD) return part;
                return {
                  ...part,
                  toolInvocation: {
                    ...part.toolInvocation,
                    result: "[search results omitted from memory]"
                  }
                };
              })
            }
          }))
        }),
        // Rewrite observations before they are persisted.
        afterObservation: ({ observations, threadId }) => ({
          observations: redact(observations),
        }),
        // Rewrite the text the Reflector condenses.
        beforeReflection: ({ observations }) => ({
          observations: stripInternalNotes(observations),
        }),
        // Rewrite the output before it's persisted, or run a side effect.
        afterReflection: async ({ observations, resourceId }) => {
          await syncToExternalStore(resourceId, observations)
        },
      },
    },
  },
})

Transform hooks are always awaited, on every path: manual observe()/reflect(), turn-synchronous observation, and async buffering. Returning void from any of them passes the input through unchanged.

For more information and full configuration options, see:

Share on X or LinkedIn
Patrycja Jenkner-Sar
Patrycja Jenkner-SarProduct Marketer & Content Engineer

Patrycja Jenkner-Sar is a product marketer and content engineer at Mastra. Previously, she was a product marketer at neptune.ai, an experiment tracker for machine learning teams, and is focused on translating complex functionality into clear value for developers and technical audiences.

All articles by Patrycja Jenkner-Sar →