> ## Documentation Index
> Fetch the complete documentation index at: https://maestro.rasa.com/llms.txt
> Use this file to discover all available pages before exploring further.

# memory.yml

> Memory schema: skill and project scopes, field types, visibility, and access control.

`memory.yml` declares what a skill remembers. Every value a tool writes must be
declared here (or in the project-level file): `rasa train` rejects an
undeclared write.

There are **two** kinds of `memory.yml`, with different shapes.

## Skill memory: `skills/<id>/memory.yml`

```yaml skills/card_replace/memory.yml theme={null}
schema:
  public:
    replacement_reason:
      type: categorical
      enum_values: [lost, stolen, damaged, not_received]
      description: Why the card is being replaced.
      llm_settable: true
    selected_card_id:
      type: text
      description: Account id of the chosen card.
  private:
    eligibility_checked:
      type: bool
      description: Whether account eligibility was verified.

access:
  deny_read: []
  deny_write: []
```

The top-level keys are **`schema:`** and **`access:`**. `schema:` splits into
`public:` and `private:`; both are optional.

## Project memory: `memory.yml` at the agent root

Values shared **across** skills live in a project-level file. Its shape is
different: a **flat map** of entry name to attributes, with no
`schema:`/`public:`/`private:` wrapper.

```yaml memory.yml theme={null}
selected_card_id:
  type: text
  description: Account id of the card the customer chose.
card_list:
  type: json
  description: Cards on the customer's account.
authenticated:
  type: bool
  description: Whether the customer has verified their identity.
```

These resolve to the `project.` namespace, so a condition refers to
`session.project.authenticated`. This is the mechanism behind cross-skill state:
one skill writes `authenticated`, another gates on it, and neither references
the other.

## Field attributes

Every attribute is optional; a bare entry (`my_field: {}`) is a valid `any` field.

| Attribute       | Default | Description                                                                              |
| --------------- | ------- | ---------------------------------------------------------------------------------------- |
| `type`          | `any`   | Value type: see below                                                                    |
| `description`   | `null`  | Shown to the LLM in `set_fields` and `correct` schemas                                   |
| `enum_values`   | `[]`    | Allowed values; meaningful with `type: categorical`                                      |
| `llm_settable`  | `false` | Whether the LLM may write this via `set_fields`                                          |
| `pii`           | `false` | Withholds the value from prompts: the LLM sees the entry as set, without the content     |
| `immutable`     | `false` | Write once; a second write raises `MemoryWriteError`                                     |
| `initial_value` | `null`  | Value the entry holds before anything writes to it, and the value it returns to on reset |

### Types

| `type`        | JSON schema type used for `set_fields`      |
| ------------- | ------------------------------------------- |
| `text`        | `string`                                    |
| `bool`        | `boolean`                                   |
| `int`         | `integer`                                   |
| `float`       | `number`                                    |
| `list`        | `array`                                     |
| `categorical` | `string`, plus an `enum` from `enum_values` |
| `json`        | `string`                                    |
| `any`         | `string`                                    |

Write the type name exactly as it appears in the left column.

### `enum_values`

```yaml theme={null}
replacement_reason:
  type: categorical
  enum_values: [lost, stolen, damaged, not_received]
```

A `categorical` field with `enum_values` gets an `enum` constraint in the
`set_fields` schema, so the LLM can only record one of the listed values. It is
also what makes the field usable in `if:` markers with confidence about the
value space.

### `llm_settable`

This is the switch that decides whether the LLM may write a value at all.

```yaml theme={null}
# the user's stated decision, recorded by the LLM
replacement_reason:
  type: categorical
  enum_values: [lost, stolen, damaged, not_received]
  llm_settable: true

# derived by a tool; the LLM must never invent it
account_replace_eligible:
  type: bool
```

The entries the LLM may set for a turn are the union of:

* fields flagged `llm_settable: true`, and
* fields owned by a `collect:` step in the active skill (settable regardless of
  the flag. The engine asked the user for them directly).

`llm_settable` also gates whether a field is offered to the `correct` tool. Leave
engine-derived values (eligibility results, lock state, computed flags) at the
default so the model can neither set nor "correct" them.

<Note>
  A project field with `llm_settable: true` is settable from **any** skill. A
  project field owned by a `collect:` step is settable only while its owning skill
  is active.
</Note>

## Visibility

| Scope                       | Readable by                             |
| --------------------------- | --------------------------------------- |
| `schema.public`             | The owning skill, and every other skill |
| `schema.private`            | The owning skill only                   |
| project (root `memory.yml`) | Every skill                             |

Concretely, while skill `A` is active it can read `A`'s own public **and**
private fields, every **other** skill's public fields, and all project fields,
minus anything listed in `A`'s `deny_read`.

`public` is your skill's API. Keep it small and stable: another skill gating on
`session.project.authenticated` should depend on that key, not on the auth
skill's internals.

## Access control

```yaml theme={null}
access:
  deny_read:
    - card_replace.card_list
  deny_write:
    - project.authenticated
```

Entries are **fully-qualified names**, `<skill_id>.<entry>` or
`project.<entry>`.

* A denied **read** returns `None`. It does not raise.
* A denied **write** raises `MemoryWriteError`.

That asymmetry is deliberate: reads happen during prompt construction and
condition evaluation, where failing quietly is safer; writes are explicit
actions whose rejection the LLM needs to see.

## Undeclared writes

`context.memory.set("foo", 1)` where `foo` is in neither the skill's `schema:`
nor the project file fails `rasa train` with `undeclared_memory_write`. Fix it
by declaring the entry with its `type`.

The same applies to a `collect:` step target and to any key named in a
`requires:` or `if:` condition.

## Fully-qualified names

The scope an entry is declared in determines its full name:

| Declared in                      | Fully-qualified name              | Referenced in conditions as               | Referenced in tools (`context.memory`) as                 |
| -------------------------------- | --------------------------------- | ----------------------------------------- | --------------------------------------------------------- |
| `skills/card_replace/memory.yml` | `card_replace.replacement_reason` | `session.card_replace.replacement_reason` | `replacement_reason` or `card_replace.replacement_reason` |
| root `memory.yml`                | `project.selected_card_id`        | `session.project.selected_card_id`        | `selected_card_id` or `project.selected_card_id`          |

While skill `card_replace` is active, a tool reads its own field with the bare
name and another skill's **public** field with the fully-qualified name:

```python theme={null}
reason = context.memory.get("replacement_reason")
selected_card_id = context.memory.get("card_disambiguate.selected_card_id")
```

`system.*` is reserved for engine internals and can never be declared.

<Note>
  The namespace is the skill **directory name**, not the `name:` in frontmatter. A
  skill in `skills/card_replace/` with `name: Card Replace` is always
  `session.card_replace.*`.
</Note>

## See also

* [Memory concept](/concepts/memory): when to declare what
* [Conditions](/reference/conditions): referencing entries in `requires:` / `if:`
* [Tools reference](/reference/tools): `context.memory` read/write rules
