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

*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:

```text
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](https://github.com/mastra-ai/mastra/issues/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](https://docs.memorysync.io/guides/mastra?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_mastra-observational-memory-propertynames-is-not-permitted) 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](https://memorysync.io/?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_mastra-observational-memory-propertynames-is-not-permitted). Founding offer: use code FOUNDING30 for 30% off Starter for your first 3 months (Settings → Billing).

## Status (Nov 13, 2026)

- [#25343](https://github.com/mastra-ai/mastra/issues/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](https://github.com/mastra-ai/mastra/pull/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](https://mastra.ai/reference/memory/observational-memory) 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:

```json
{"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)

```bash
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](https://mastra.ai/reference/memory/observational-memory) says "Schema-less extractors are requested inline in the Observer output", so there's no separate structured call to reject:

```ts
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](https://mastra.ai/docs/memory/working-memory): 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](https://mastra.ai/docs/agents/structured-output)). 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](https://github.com/mastra-ai/mastra/issues/25343):

```ts
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](https://docs.memorysync.io/guides/mastra?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_mastra-observational-memory-propertynames-is-not-permitted) 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):**

```bash
npm install memorysync-mastra @mastra/core
```

```ts
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](https://docs.memorysync.io/guides/mastra?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_mastra-observational-memory-propertynames-is-not-permitted); I haven't run it for this page.* For a longer walkthrough, see [Add long-term memory to a Mastra agent](https://memorysync.hashnode.dev/mastra-agent-long-term-memory-processors).

**Try it:** MemorySync is optional here: the steps above work without it. If you want the memory layer from the snippet, [see MemorySync](https://memorysync.io/?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_mastra-observational-memory-propertynames-is-not-permitted), 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](https://memorysync.hashnode.dev/mastra-working-memory-tool-call-messages), and for a missing storage error see [Mastra 'Memory requires a storage provider to function'](https://memorysync.hashnode.dev/mastra-memory-requires-a-storage-provider).

## 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+).

## Related pages

- [Mastra working memory with tool calls: fixing 'use: tool-call' and messages missing before tool calls](https://memorysync.hashnode.dev/mastra-working-memory-tool-call-messages)
- [Add long-term memory to a Mastra agent (input/output processors, per-user recall)](https://memorysync.hashnode.dev/mastra-agent-long-term-memory-processors)
- [Mastra 'Memory requires a storage provider to function': fixing it in tests, evals and CI](https://memorysync.hashnode.dev/mastra-memory-requires-a-storage-provider)

## 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](https://memorysync.io/?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_mastra-observational-memory-propertynames-is-not-permitted).

**Next step:** read the [MemorySync Mastra guide](https://docs.memorysync.io/guides/mastra?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_mastra-observational-memory-propertynames-is-not-permitted), then [see MemorySync](https://memorysync.io/?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_mastra-observational-memory-propertynames-is-not-permitted). Founding offer: use code FOUNDING30 for 30% off Starter for your first 3 months (Settings → Billing).

