mastra-ai/mastra · error · Error

Hook extractor "${name}" must include an onExtracted handler

Error message

Hook extractor "${name}" must include an onExtracted handler.

What it means

Hook extractors run as post-extraction callbacks and do not participate in the extraction prompt, so they have no use for `instructions` but must provide an `onExtracted` handler to do their work. The library throws this in the constructor when `mode: 'hook'` is set but `config.onExtracted` is missing or falsy.

Source

Thrown at packages/memory/src/processors/observational-memory/extractor.ts:155

  private readonly schemaConfig?: ExtractorConfigValue<z.ZodType<T> | undefined>;

  constructor(config: ExtractorConfig<T>, internal = false) {
    const name = config.name.trim();
    const instructions = typeof config.instructions === 'string' ? config.instructions.trim() : undefined;
    const slug = slugifyExtractorName(name);
    const isHook = config.mode === 'hook';

    if (!name) {
      throw new Error('Extractor name is required.');
    }
    if (!isHook && !config.instructions) {
      throw new Error(`Extractor "${name}" must include instructions.`);
    }
    if (instructions !== undefined && !instructions) {
      throw new Error(`Extractor "${name}" must include instructions.`);
    }
    if (isHook && !config.onExtracted) {
      throw new Error(`Hook extractor "${name}" must include an onExtracted handler.`);
    }
    if (isHook && (config.instructions || config.schema)) {
      throw new Error(`Hook extractor "${name}" cannot include instructions or a schema.`);
    }
    assertValidSlug(slug, name);
    if (!internal && RESERVED_XML_TAGS.has(slug)) {
      throw new Error(`Extractor slug "${slug}" is reserved by Observational Memory.`);
    }

    this.name = name;
    this.slug = slug;
    this.instructionsConfig = config.instructions ?? '';
    this.schemaConfig = config.schema;
    this.instructions = instructions ?? '';
    this.schema = (
      isHook ? z.unknown() : typeof config.schema === 'function' ? z.string() : (config.schema ?? z.string())
    ) as z.ZodType<T>;
    this.mode = isHook ? 'hook' : internal || !config.schema ? 'inline' : 'structured';

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Provide an `onExtracted` callback that receives ExtractorOnExtractedContext and returns the (possibly transformed) value.
  2. If the extractor should drive the LLM prompt instead, remove `mode: 'hook'` and add `instructions`.
  3. Verify the property is spelled `onExtracted` and is not being stripped by a spread/conditional.

Example fix

// before
new Extractor({ name: 'Audit Log', mode: 'hook' });
// after
new Extractor({
  name: 'Audit Log',
  mode: 'hook',
  onExtracted: ({ current }) => {
    logger.info('extracted', current);
  },
});
Defensive patterns

Strategy: validation

Validate before calling

if (config.mode === 'hook' && typeof config.onExtracted !== 'function') {
  throw new Error(`Hook extractor "${config.name}" needs onExtracted`);
}

Type guard

function isValidHookExtractor(c) {
  return c.mode !== 'hook' || typeof c.onExtracted === 'function';
}

Try / catch

try {
  const extractor = new Extractor(config);
} catch (e) {
  if (e.message.includes('onExtracted handler')) {
    logger.error('Hook extractor missing onExtracted', { name: config.name });
  } else { throw e; }
}

Prevention

When it happens

Trigger: new Extractor({ name: 'Audit', mode: 'hook' }) with no onExtracted; onExtracted explicitly set to undefined/null; typo in the property name (e.g. onExtract) so the handler is never seen.

Common situations: Converting a normal extractor to hook mode by only changing `mode`; copying a config type but forgetting the callback; conditionally omitting onExtracted with a spread that drops the key.

Related errors


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/874c7dbecbb548de. Report an issue: GitHub.