Skip to main content

Mastra working memory: fixing 'propertyNames' is not permitted (OpenAI 400) with Observational Memory

With a working-memory schema and manageWorkingMemory on an OpenAI model, every extraction hits a 400 and falls back. Here's the version that fixes it, what to do if you can't upgrade, and how to check.

Updated 13 min readMarkdown

Disclosure: I work on MemorySync (I'm Mohammed, the founder). I drafted this article with AI assistance and checked it against the GitHub issue, the fix PR, the Mastra docs, the published npm packages and the MemorySync docs listed under Sources on Oct 31, 2026, and re-checked them on Nov 13, 2026. Code status: I ran the issue's reproduction (schema working memory plus manageWorkingMemory: true on openai/gpt-5.4-mini) on @mastra/core 1.71.0, 1.77.0, 1.78.0 and 1.80.0 (Oct 31) and on 1.83.0 with @mastra/memory 1.41.1 (Nov 13) against a local mock of OpenAI's Responses API, plus a plain structuredOutput script with a z.record() field. The mock answers the same 400 for a strict JSON schema that contains propertyNames and a canned 200 for everything else. It models only that one OpenAI rule, so it shows what Mastra sends, not how OpenAI judges every schema. No OpenAI key and no request to OpenAI. I didn't run anything against a real model, the fetch-logging snippet in Step 4, or the MemorySync snippet.

You turned on Observational Memory with manageWorkingMemory: true, gave working memory a Zod schema, used an OpenAI model, and your logs started showing an Upstream LLM API error like this on every extraction:

400 invalid_json_schema: Invalid schema for response_format 'response':
In context=('properties', 'working-memory', 'anyOf', '0'), 'propertyNames' is not permitted.

That's the error in Mastra issue #25343, "OM manageWorkingMemory with schema working memory: structured extraction always 400s on OpenAI ('propertyNames' is not permitted), then falls back", reported on @mastra/core 1.71.0 with @mastra/memory 1.32.1 (and also seen on 1.67.0 / 1.30.0).

Short version: upgrade @mastra/core to 1.78.0 or later. If you can't yet, switch working memory to a Markdown template or let the agent manage it. Details and the checks I ran are below.

MemorySync doesn't fix this bug. The fix is in Mastra's own packages, and nothing on the MemorySync side changes it. MemorySync comes up once, near the end, as a separate layer for per-user memory.

Skip the memory plumbing. The MemorySync Mastra guide shows input and output processors that add per-user memory to a Mastra agent next to Mastra's own Memory. It doesn't touch Observational Memory and doesn't fix this 400. When you're ready, see MemorySync. Founding offer: use code FOUNDING30 for 30% off Starter for your first 3 months (Settings → Billing).

Status (Nov 13, 2026)

  • #25343 was opened Sep 28, 2026 and closed as completed on Oct 15, 2026 (00:04 IST), and was still closed when I re-checked on Nov 13, 2026. It was closed by PR #26979, "fix: accept null for optional anyOf tool inputs and support z.record() in OpenAI structured output".
  • The PR changed @mastra/core and @mastra/schema-compat, not @mastra/memory. Its changeset says schemas with a field like z.record(z.string(), z.string()) "now works on OpenAI instead of failing with a 400 error. These schemas are sent without strict mode."
  • By my check of the npm tarballs, the fix code first ships in @mastra/core 1.78.0 (published Oct 16, 2026, 04:07 IST). 1.77.0 was published before the merge and doesn't have it. The latest on Nov 13, 2026 is 1.83.0 (published Nov 13, 02:57 IST), and my re-run on it gave the same result as 1.80.0.

Step 1: Confirm it's this bug

You're looking at this bug if all of these are true:

  1. workingMemory.schema is set (a Zod object, not a Markdown template).
  2. observationalMemory.observation.manageWorkingMemory is true.
  3. The Observer model is a native OpenAI model (for example openai/gpt-5.4-mini through the model router).
  4. Your @mastra/core is 1.77.0 or older.

Why the error names working-memory when your schema has no such field: with manageWorkingMemory, Mastra's Observational Memory reference says the setting "Adds WorkingMemoryExtractor". In @mastra/memory 1.32.1 that extractor doesn't send your schema to the model. It sends a placeholder, a z.record(z.string(), z.unknown()) (or null), wrapped under a working-memory key. Here's the schema my reproduction captured on 1.71.0:

{"type":"object","properties":{"working-memory":{"anyOf":[
  {"type":"object","propertyNames":{"type":"string"},"additionalProperties":false},
  {"type":"null"}]}},"additionalProperties":false,"required":["working-memory"]}

z.record() becomes propertyNames, and the request goes out with strict: true. The issue reports that OpenAI rejects that, and the PR author's live check says strict mode fails for a record "both with and without propertyNames" while non-strict succeeds.

Two details make it easy to dismiss:

  • Working memory still saves. Mastra catches the 400 and retries the extraction with the schema injected into the prompt instead. In the issue, and in my mock run, the stored working memory came out right.
  • So the cost is one failed request per extraction and an error in your logs each time, as the issue puts it. That looks like a production failure, and saving now depends on the fallback.

Step 2: Upgrade @mastra/core to 1.78.0 or later (the fix)

npm install @mastra/core@latest

Then run your usual checks. Here's what my reproduction (the issue's script, three turns, Observer at messageTokens: 400) sent to the mock on each version, in order:

@mastra/core / @mastra/memory Requests to the mock, in order Upstream LLM API error logs Stored working memory
1.71.0 / 1.32.1 agent 200, observer 200, structured strict: true 400, prompt-injected fallback 200, observer 200, observer 200 1 {"preferredName":"Priya","reportCurrency":"USD"}
1.77.0 / 1.32.1 same as 1.71.0 1 same
1.78.0 / 1.32.1 agent 200, observer 200, structured strict: false 200, observer 200, observer 200 0 same
1.80.0 / 1.39.1 same as 1.78.0 0 same
1.83.0 / 1.41.1 (run Nov 13) same as 1.78.0 0 same

The mock returned 400 only for a strict schema containing propertyNames, so the 200 on 1.78.0 shows Mastra stopped sending strict mode for this schema. That OpenAI accepts the non-strict request comes from the PR's live run, not from mine.

Two version notes:

  • You don't have to upgrade @mastra/memory. The fix is in core. In my run, @mastra/core 1.78.0 with @mastra/memory 1.32.1 was enough (1.32.1 declares a peer range of @mastra/core >=1.4.1-0 <2.0.0-0).
  • If you do upgrade @mastra/memory, match its core peer range. 1.39.1 declares >=1.80.0-0 <2.0.0-0, and 1.41.1 (the latest on Nov 13, 2026) declares >=1.83.0-0 <2.0.0-0.

Step 3: If you can't upgrade yet

Pick one. Both avoid the structured extraction call that fails.

Option A: use a Markdown template for working memory. Without a schema, the extractor has no structured schema. The reference says "Schema-less extractors are requested inline in the Observer output", so there's no separate structured call to reject:

const memory = new Memory({
  storage,
  options: {
    workingMemory: {
      enabled: true,
      scope: "resource",
      template: "# User\n- Name:\n- Report currency:\n",
    },
    observationalMemory: {
      model: "openai/gpt-5.4-mini",
      observation: { manageWorkingMemory: true },
    },
  },
});

On 1.71.0 (and again on 1.83.0) my mock run made no structured request and logged no 400. (The stored text came from the canned Observer reply in my mock, so it shows the request path, not extraction quality.) The trade-off is in Mastra's working memory docs: templates use "replace semantics", while a schema gives "structured, type-safe JSON data that supports validation", and "setting both template and schema isn't supported".

Option B: keep the schema and turn manageWorkingMemory off. Then the main agent updates working memory itself, through its working memory tool. On 1.71.0 (and again on 1.83.0) my mock run made no structured request. The mock agent never calls tools, so nothing was stored in that run; with a real model, whether working memory updates depends on the agent calling the tool, which is the dependency manageWorkingMemory was meant to remove.

The same 400 outside memory

The fix isn't specific to working memory. Any structuredOutput schema with a z.record() field on a native OpenAI model hit the same rule before 1.78.0, and a plain agent call has no fallback. My script asked for z.object({ labels: z.record(z.string(), z.string()) }):

@mastra/core structuredOutput setting Result against the mock
1.71.0 default threw: 'propertyNames' is not permitted (context=('properties', 'labels'))
1.71.0 jsonPromptInjection: true object returned (schema went in the prompt, no response_format)
1.71.0 fixed keys instead of z.record() object returned (strict request)
1.80.0 default object returned (strict: false request)
1.83.0 (run Nov 13) default object returned (strict: false request)

On older versions, either list the keys you expect or set jsonPromptInjection (see Mastra's structured output docs). My mock only checks for propertyNames, so for the fixed-keys row it shows the request no longer contains it, not that OpenAI accepts every such schema.

Step 4: Check that the 400 is gone

The issue's reporter found it by wrapping fetch and logging each OpenAI request that carries a JSON schema. A cut-down version, adapted from the script in #25343:

const realFetch = globalThis.fetch;
globalThis.fetch = async (input, init) => {
  const res = await realFetch(input, init);
  const body = typeof init?.body === "string" ? init.body : "";
  if (String(input).includes("api.openai.com") && body.includes('"json_schema"')) {
    const format = JSON.parse(body).text?.format;
    console.log("json_schema request", res.status, "strict:", format?.strict);
  }
  return res;
};

Put it at the top of a dev script, send a few long turns so the Observer runs, and look for a status of 400. After upgrading you should see 200 with strict: false for the working-memory extraction, and no Upstream LLM API error with propertyNames in your logs. I ran the same logic inside my mock, not this snippet against OpenAI.

Skip the plumbing with MemorySync

Disclosure: I build MemorySync.

This doesn't fix the 400 or change Observational Memory. It's a different way to keep durable per-user facts, if that's what you were using schema working memory for. The MemorySync Mastra guide documents createMemorySyncProcessors: an input processor that injects recalled context as a system message before the model call, and an output processor that sends the user's message to MemorySync after it, "where only the durable facts in it are stored; assistant replies are not stored". According to the guide, it works on @mastra/core 1.42+, and it's meant to run alongside Mastra's Memory: "Memory for the transcript window, MemorySync for what the agent should permanently know."

From the guide:

Unverified (not run):

npm install memorysync-mastra @mastra/core
import { Agent } from "@mastra/core/agent";
import { openai } from "@ai-sdk/openai";
import { createMemorySyncProcessors } from "memorysync-mastra";

const { inputProcessor, outputProcessor } = createMemorySyncProcessors({
  userId: "customer-7",     // per-end-user scoping
  sessionId: "thread-42",   // groups the stored facts by thread
});

const agent = new Agent({
  id: "assistant",
  name: "Assistant",
  instructions: "You are a helpful assistant.",
  model: openai("your-model"),
  inputProcessors: [inputProcessor],
  outputProcessors: [outputProcessor],
});

The guide also lists four ways to resolve which user a call is for, including the memory: { resource, thread } you already pass to generate. Snippet adapted from the MemorySync Mastra guide; I haven't run it for this page. For a longer walkthrough, see Add long-term memory to a Mastra agent.

Try it: MemorySync is optional here: the steps above work without it. If you want the memory layer from the snippet, see MemorySync, create an API key in the dashboard and drop the snippet into your agent.

Troubleshooting

  • Still a 400 after upgrading. Check the version that's actually loaded (npm ls @mastra/core); a lockfile or a second copy can keep an old core. The fix needs 1.78.0 or later.
  • npm install complains about a peer dependency after upgrading @mastra/memory. @mastra/memory 1.39.1 declares @mastra/core >=1.80.0-0, and 1.41.1 declares >=1.83.0-0. Upgrade core too, or keep @mastra/memory where it was (Step 2).
  • You want strict mode anyway. The changeset says "set providerOptions.openai.strictJsonSchema: true" to keep it. Per the PR's raw API probes, strict mode with a record schema fails, so expect the 400 back.
  • The error is from your own structuredOutput call, not memory. Same cause; see "The same 400 outside memory".
  • Working memory comes back empty with a Markdown template. That's a different problem from this one; for working-memory tool-call issues see Mastra working memory with tool calls, and for a missing storage error see Mastra 'Memory requires a storage provider to function'.

Common questions

Did I lose any working memory? Not from this bug, as far as the issue and my mock runs show: the prompt-injected fallback saved the right values. The cost was the failed request and the log noise.

Why does the error mention working-memory and anyOf when my schema has neither? They come from Mastra's extractor wrapper (Step 1), not from your schema. In 1.32.1 your schema only reaches the model as text in the extractor's instructions.

Does this happen with Anthropic or Gemini models? The error is OpenAI's strict structured-output check, and the switch to non-strict in the fix applies to native OpenAI providers. I only tested the openai/ model router path.

Do I need a new @mastra/memory version? No. Core 1.78.0+ is the fix. Upgrading @mastra/memory is fine too, but match its peer range (1.39.1 needs core 1.80.0+, 1.41.1 needs 1.83.0+).

Sources

  • github.com/mastra-ai/mastra/issues/25343 (opened 2026-09-28, closed as completed 2026-10-14 UTC / Oct 15 IST, 4 comments; exact error, reproduction script, captured schema and cause; read via the GitHub API 2026-10-31; state, close date and comment count re-checked 2026-11-13)
  • github.com/mastra-ai/mastra/pull/26979 (merged 2026-10-14 UTC; changeset for @mastra/core and @mastra/schema-compat; live-verification notes; read 2026-10-31; merged state re-checked 2026-11-13)
  • npm: @mastra/core 1.71.0, 1.77.0, 1.78.0, 1.80.0 and @mastra/memory 1.32.1, 1.39.1 (publish times, peer ranges, and a check of which core tarballs contain the fix, 2026-10-31); @mastra/core 1.83.0 and @mastra/memory 1.41.1, the latest on 2026-11-13 (publish times and peer ranges, 2026-11-13)
  • Mastra docs: Observational Memory, Observational Memory reference, Working memory, Structured output (fetched 2026-10-31, quotes re-checked 2026-11-13)
  • Offline runs (2026-10-31): the issue's reproduction on @mastra/core 1.71.0 / 1.77.0 / 1.78.0 with @mastra/memory 1.32.1 and on 1.80.0 / 1.39.1, schema vs template vs manageWorkingMemory: false; a structuredOutput script with z.record(), jsonPromptInjection and fixed keys on 1.71.0 and 1.80.0; all against a local mock of OpenAI's Responses API. Repeated on @mastra/core 1.83.0 / @mastra/memory 1.41.1 (2026-11-13): schema, template, manageWorkingMemory: false, and the z.record() / jsonPromptInjection / fixed-keys script
  • MemorySync Mastra guide: docs.memorysync.io/guides/mastra (fetched 2026-10-31, quotes re-checked 2026-11-13)

Try it

For an overview of MemorySync, see the MemorySync homepage.

Next step: read the MemorySync Mastra guide, then see MemorySync. Founding offer: use code FOUNDING30 for 30% off Starter for your first 3 months (Settings → Billing).

About the author

  • Building MemorySync (memorysync.io) — persistent memory infrastructure for AI agents, Cursor, and Claude Code.

No comments yet. Be the first to comment.