# Giving an Agent Memory

Enable long-term memory on a session agent, watch it save, recall, search, and pin memories, and inspect and delete its records through the API.

In this tutorial you give a session agent long-term memory. The agent keeps notes about payout journals: each journal is its own session. In the first session it saves a fact about that journal and a reporting rule for every journal, and pins the rule. You then open a second session and see which memories cross over, run a later turn of the first session that recalls and searches what it saved, search the session's history, and finally read and delete records through the API.

The agent uses the `fixture` model: a script of model responses that Twisp plays back in order. The script also answers the review that runs after each turn to pick out what is worth remembering, so every run gives the same result and nothing calls a real model. An optional last step runs that review with a Claude model.

> **Note:**
>
> You need a running [local environment](https://www.twisp.com/docs/infrastructure/local-environment.md). Open GraphiQL in the local console and run each step as its own request. If you have not built an agent before, start with [Your First Agent](https://www.twisp.com/docs/tutorials/actions/first-agent.md).

The UUIDs below are examples. If you have run this tutorial before, choose fresh UUIDs for the agent and the journals. Execution and memory IDs differ on every run: wherever a step uses one, substitute the ID from your own earlier response.

## 1. Create the agent

```graphql
mutation CreateMemoryAgent {
  actions {
    createAction(
      id: "2afe6c28-d18d-4f3e-801d-352740eb8925"
      input: {
        name: "agent-memory-notes"
        kind: "agent"
        agent: {
          prompt: "You keep notes about payout journals. Remember what you learn about each journal, and how operators want payouts reported."
        }
        triggers: [
          {
            name: "opened"
            event: "event.type == 'journal.inserted' && event.data.name.startsWith('agent-memory-')"
            session: {
              start: {
                input: { journal_id: "string(event.data.journal_id)", name: "event.data.name" }
              }
            }
          }
          {
            name: "updated"
            event: "event.type == 'journal.updated' && event.data.name.startsWith('agent-memory-')"
            session: { resume: { id: "string(event.data.journal_id)" } }
          }
        ]
        session: {
          inputSchema: {
            type: "object"
            properties: { journal_id: { type: "string" }, name: { type: "string" } }
            required: ["journal_id", "name"]
          }
          id: "session.input.journal_id"
        }
        config: {
          runtime: "strands"
          harness: {
            memory: {
              enabled: true
              scope: { read: ["session", "agent"], write: ["session", "agent"] }
              tools: ["add_memory", "read_memory", "delete_memory"]
              pinned: { max_tokens: 500 }
            }
            history: { index: "messages" }
          }
          model: {
            provider: "fixture"
            script: [
              {
                when: "purpose == 'turn' && session.step == 0 && session.input.name == 'agent-memory-north'"
                responses: [
                  [
                    {
                      toolUse: {
                        name: "add_memory"
                        input: { summary: "This journal pays out on Fridays.", kind: "fact" }
                      }
                    }
                  ]
                  [
                    {
                      toolUse: {
                        name: "add_memory"
                        input: {
                          summary: "Report payout amounts in USD with two decimals."
                          kind: "guidance"
                          scope: "agent"
                          pinned: true
                        }
                      }
                    }
                  ]
                  [{ text: "Saved what I learned about this journal." }]
                ]
              }
              {
                when: "purpose == 'turn' && session.step > 0"
                responses: [
                  [
                    {
                      toolUse: {
                        name: "search_memory"
                        input: { query: "When does this journal pay out?" }
                      }
                    }
                  ]
                  [{ toolUse: { name: "search_history", input: { query: "weekly payouts" } } }]
                  [{ text: "Checked my notes and this journal's history." }]
                ]
              }
              {
                when: "purpose == 'turn'"
                responses: [
                  [
                    {
                      toolUse: {
                        name: "search_memory"
                        input: { query: "When does this journal pay out?" }
                      }
                    }
                  ]
                  [{ text: "Checked my notes." }]
                ]
              }
              {
                when: "purpose == 'memory' && session.step == 0 && session.input.name == 'agent-memory-north'"
                responses: [
                  [
                    {
                      text: "[{\"summary\": \"The north journal was opened for weekly payouts.\", \"kind\": \"event\"}]"
                    }
                  ]
                  [{ text: "[{\"candidate\": 1, \"action\": \"add\"}]" }]
                ]
              }
              { when: "purpose == 'memory'", responses: [[{ text: "[]" }]] }
              {
                when: "purpose == 'outcome'"
                responses: [{ outcome: { reply: "Done.", session: { status: "OPEN" } } }]
              }
            ]
          }
        }
      }
    ) {
      actionId
      version
      status
      pending
    }
  }
}
```

The response reports `status: UNPUBLISHED` and `pending: ACTIVE`. What the definition sets up:

- **Sessions.** Inserting a journal whose name starts with `agent-memory-` starts a session for that journal, and updating it runs another turn of the same session.
- **`harness.memory`.** `enabled` turns memory on. `scope` lets the agent read and write both scopes: `session` records stay with the journal that wrote them, and `agent` records reach every journal's session. `tools` gives the agent `add_memory`, `read_memory`, and `delete_memory` besides `search_memory`, and `pinned` lets it pin records within a 500-token budget.
- **`harness.history`.** `index: "messages"` keeps each turn's message and the agent's replies searchable with `search_history`.
- **The script.** Each model call takes the next response from the first entry whose `when` matches. The journal name is part of the script: the first turn of the session for `agent-memory-north` saves two memories, any later turn searches memory and history, and any other first turn only searches memory.

| Entry `when`                             | What it plays                                                                                        |
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `purpose == 'turn'`, north, first turn   | `add_memory` of a session fact, `add_memory` of a pinned agent rule, then a reply.                   |
| `purpose == 'turn' && session.step > 0`  | `search_memory`, `search_history`, then a reply.                                                     |
| `purpose == 'turn'`                      | `search_memory`, then a reply.                                                                       |
| `purpose == 'memory'`, north, first turn | The review after the turn: one candidate memory, then the decision to add it.                        |
| `purpose == 'memory'`                    | The review after any other turn: nothing worth remembering.                                          |
| `purpose == 'outcome'`                   | The [turn outcome](https://www.twisp.com/docs/reference/actions/agents.md#turn-outcome): reply `Done.` and keep the session open. |

The `memory` entries stand in for the model calls that review each turn. The first call returns candidate memories as a JSON array. When a candidate resembles memories already kept, a second call decides whether to add it, replace one, delete one, or skip it.

Poll until the agent is live:

```graphql
query ReadMemoryAgent {
  actions {
    action(id: "2afe6c28-d18d-4f3e-801d-352740eb8925") {
      version
      status
      pending
    }
  }
}
```

```json
{ "version": 2, "status": "ACTIVE", "pending": null }
```

## 2. Start the first session

Create the north journal. Its insert starts a session whose ID is the journal ID, and the first turn runs at once.

```graphql
mutation OpenNorthJournal {
  createJournal(
    input: {
      journalId: "c131e037-1afb-4f10-ba38-ee4956458f79"
      name: "agent-memory-north"
      description: "Weekly payouts"
    }
  ) {
    journalId
    version
  }
}
```

Read the session's turns:

```graphql
query NorthSession {
  actions {
    action(id: "2afe6c28-d18d-4f3e-801d-352740eb8925") {
      session(id: "c131e037-1afb-4f10-ba38-ee4956458f79") {
        status
        turns(first: 10) {
          nodes {
            number
            status
            executionId
            execution {
              status
              invocation {
                output
              }
            }
          }
        }
      }
    }
  }
}
```

Within a few seconds, turn 1 is `COMMITTED`. Keep its `executionId` for the next step:

```json
{
  "status": "OPEN",
  "turns": {
    "nodes": [
      {
        "number": 1,
        "status": "COMMITTED",
        "executionId": "12a4007b-ac15-5163-ae66-7184f23adfc4",
        "execution": { "status": "SUCCEEDED", "invocation": { "output": "Done." } }
      }
    ]
  }
}
```

## 3. Read what the turn saved

The turn's [transcript](https://www.twisp.com/docs/reference/actions/agents.md#transcripts) records every model call and tool call:

```graphql
query TurnTranscript($id: UUID!) {
  actions {
    execution(id: $id) {
      invocation {
        transcript(first: 100) {
          nodes {
            sequence
            kind
            data
          }
        }
      }
    }
  }
}
```

Run it with `{"id": "12a4007b-ac15-5163-ae66-7184f23adfc4"}`. The transcript is long; look for these events:

- Two `tool.output` events named `add_memory`. Each returns the record it stored, with its ID and scope. The first is a `session` record; the second, which the script gave `scope: "agent"` and `pinned: true`, is an `agent` record:

  ```json
  {
    "name": "add_memory",
    "output": {
      "content": [
        {
          "text": "{\"stored\": {\"id\": \"ede55d89-f542-590f-8f6d-ddcf25947ce6\", \"summary\": \"Report payout amounts in USD with two decimals.\", \"kind\": \"guidance\", \"scope\": \"agent\", \"pinned\": true, \"created\": \"2026-10-11T17:54:25.140602Z\"}}"
        }
      ],
      "memories": [
        { "memory_id": "ede55d89-f542-590f-8f6d-ddcf25947ce6", "summary": "Report payout amounts in USD with two decimals.", "content": "" }
      ],
      "status": "success"
    },
    ...
  }
  ```

- After the outcome, two `model.response` events with `"purpose": "memory"`: the review of the turn. The first proposes a candidate, and the second, because the candidate resembles the records the turn just saved, decides to add it:

  ```json
  { "purpose": "memory", "content": [{ "text": "[{\"summary\": \"The north journal was opened for weekly payouts.\", \"kind\": \"event\"}]" }], ... }
  { "purpose": "memory", "content": [{ "text": "[{\"candidate\": 1, \"action\": \"add\"}]" }], ... }
  ```

- `usage` events with `"purpose": "embed"`: each memory search, including the review's comparison with existing memories, embeds its query so records can be found by meaning. No limit counts them.

## 4. List the agent's memory

`Action.memories` lists the agent's records, newest first. A record appears shortly after the turn that wrote it commits.

```graphql
query AgentMemories {
  actions {
    action(id: "2afe6c28-d18d-4f3e-801d-352740eb8925") {
      memories(first: 20) {
        nodes {
          memoryId
          summary
          kind
          scope
          source
          pinned
          status
          sessionId
        }
      }
    }
  }
}
```

```json
{
  "nodes": [
    {
      "memoryId": "3c41d8a2-8d97-5263-b7a0-ff103bb6cf17",
      "summary": "The north journal was opened for weekly payouts.",
      "kind": "event",
      "scope": "SESSION",
      "source": "EXTRACTION",
      "pinned": false,
      "status": "ACTIVE",
      "sessionId": "c131e037-1afb-4f10-ba38-ee4956458f79"
    },
    {
      "memoryId": "ede55d89-f542-590f-8f6d-ddcf25947ce6",
      "summary": "Report payout amounts in USD with two decimals.",
      "kind": "guidance",
      "scope": "AGENT",
      "source": "TOOL",
      "pinned": true,
      "status": "ACTIVE",
      "sessionId": "c131e037-1afb-4f10-ba38-ee4956458f79"
    },
    {
      "memoryId": "a5476143-b73e-5d8c-9135-4b8420b120cd",
      "summary": "This journal pays out on Fridays.",
      "kind": "fact",
      "scope": "SESSION",
      "source": "TOOL",
      "pinned": false,
      "status": "ACTIVE",
      "sessionId": "c131e037-1afb-4f10-ba38-ee4956458f79"
    }
  ]
}
```

`source` says what wrote each record: `TOOL` for the agent's own `add_memory` calls, and `EXTRACTION` for the review after the turn. The review wrote session scope because its candidate did not ask for agent scope. Every record names the session that wrote it, whatever its scope.

## 5. Start a second session

Create the south journal. Its insert starts a second session, whose first turn only searches memory.

```graphql
mutation OpenSouthJournal {
  createJournal(
    input: {
      journalId: "3588d9bf-bdb4-4ad4-8c27-5759f80c4e8b"
      name: "agent-memory-south"
      description: "Weekly payouts"
    }
  ) {
    journalId
    version
  }
}
```

Read the session with `NorthSession`, replacing the session ID with `3588d9bf-bdb4-4ad4-8c27-5759f80c4e8b`, and run `TurnTranscript` on its turn 1 execution. Three events show what crossed over:

- The first event is `memory.pinned`: the pinned rule, listed in this turn's instructions though another session wrote it.

  ```json
  { "memories": [{ "memory_id": "ede55d89-f542-590f-8f6d-ddcf25947ce6", "memory_version": 1 }] }
  ```

- A `tool.input` and `tool.output` pair named `search_memory` with `"recall": true`: before its first model call, the agent recalled the records that best match the turn. It found only the agent-scope rule.
- The agent's own `search_memory` call, for `When does this journal pay out?`, also returns only the rule:

  ```json
  {
    "name": "search_memory",
    "output": {
      "content": [
        {
          "text": "{\"memories\": [{\"id\": \"ede55d89-f542-590f-8f6d-ddcf25947ce6\", \"summary\": \"Report payout amounts in USD with two decimals.\", \"kind\": \"guidance\", \"scope\": \"agent\", \"pinned\": true, \"score\": 1.0, \"created\": \"2026-10-11T17:54:25.140602Z\"}]}"
        }
      ],
      ...
    }
  }
  ```

The north journal's Friday payouts are `SESSION` records, so the south session never sees them. Only what the agent saved with agent scope informs every session.

## 6. Recall and search in a later turn

Update the north journal. The update resumes the north session and runs its turn 2.

```graphql
mutation UpdateNorthJournal {
  updateJournal(
    id: "c131e037-1afb-4f10-ba38-ee4956458f79"
    input: { description: "Weekly payouts, reviewed" }
  ) {
    journalId
    version
  }
}
```

Run `NorthSession` again for turn 2's `executionId`, then `TurnTranscript` on it:

- `memory.pinned` lists the rule again, and the recall before the first model call finds all three records.
- The `search_memory` call returns the north session's records and the rule, ranked by how well each matches the query:

  ```json
  {
    "memories": [
      { "id": "a5476143-b73e-5d8c-9135-4b8420b120cd", "summary": "This journal pays out on Fridays.", "kind": "fact", "scope": "session", "score": 1.0, ... },
      { "id": "3c41d8a2-8d97-5263-b7a0-ff103bb6cf17", "summary": "The north journal was opened for weekly payouts.", "kind": "event", "scope": "session", "score": 0.9934, ... },
      { "id": "ede55d89-f542-590f-8f6d-ddcf25947ce6", "summary": "Report payout amounts in USD with two decimals.", "kind": "guidance", "scope": "agent", "pinned": true, "score": 0.5878, ... }
    ]
  }
  ```

- The `search_history` call searches the session's earlier turns. Each result names the turn and who it is from, and its text is what that turn said. Turn 1's message, which carried the journal's `Weekly payouts` description, comes first; the agent's replies from turn 1 follow, each with the tools it called:

  ```json
  {
    "events": [
      { "id": "b17eac4c-b93f-53a9-b47a-22d9449ae721", "turn": 1, "text": "turn 1, user\n<turn session=\"c131e037-1afb-4f10-ba38-ee4956458f79\" step=\"0\" ...", "score": 1.0, "execution_id": "12a4007b-ac15-5163-ae66-7184f23adfc4", "sequence": 2 },
      { "id": "bc6a3704-1609-5148-a4ef-cbfbad68aabf", "turn": 1, "text": "turn 1, assistant\n[called add_memory: {\"kind\":\"guidance\",\"pinned\":true,\"scope\":\"agent\",\"summary\":\"Report payout amounts in USD with two decimals.\"}]", ... },
      ...
    ]
  }
  ```

Memory holds what the agent chose to keep, across sessions when its scope allows. History holds what one session's turns said, and only that session searches it.

## 7. Inspect a record

`agentMemory` reads one record by its agent and ID, including where it came from. Use the `memoryId` of the Friday fact from your step 4 response:

```graphql
query OneMemory {
  actions {
    agentMemory(
      actionId: "2afe6c28-d18d-4f3e-801d-352740eb8925"
      id: "a5476143-b73e-5d8c-9135-4b8420b120cd"
    ) {
      summary
      status
      scope
      source
      version
      sessionId
      executionId
      actionVersion
      modifiedBy
      execution {
        status
      }
    }
  }
}
```

```json
{
  "summary": "This journal pays out on Fridays.",
  "status": "ACTIVE",
  "scope": "SESSION",
  "source": "TOOL",
  "version": 1,
  "sessionId": "c131e037-1afb-4f10-ba38-ee4956458f79",
  "executionId": "12a4007b-ac15-5163-ae66-7184f23adfc4",
  "actionVersion": 2,
  "modifiedBy": "arn:twisp:database:::action/2afe6c28-d18d-4f3e-801d-352740eb8925",
  "execution": { "status": "SUCCEEDED" }
}
```

`executionId` is turn 1 of the north session, and `modifiedBy` is the agent's own principal: the agent wrote it.

## 8. Delete a record

Suppose the north journal moved its payouts. Delete the stale fact:

```graphql
mutation ForgetFridays {
  actions {
    deleteAgentMemory(
      actionId: "2afe6c28-d18d-4f3e-801d-352740eb8925"
      id: "a5476143-b73e-5d8c-9135-4b8420b120cd"
    ) {
      summary
      status
      version
      modifiedBy
    }
  }
}
```

```json
{
  "summary": "This journal pays out on Fridays.",
  "status": "DELETED",
  "version": 2,
  "modifiedBy": "arn:aws:iam::000000000000:user/twisp"
}
```

Run `AgentMemories` again: the Friday fact is gone, and the other two records remain. The deleted record is still readable, with every version:

```graphql
query MemoryHistory {
  actions {
    agentMemory(
      actionId: "2afe6c28-d18d-4f3e-801d-352740eb8925"
      id: "a5476143-b73e-5d8c-9135-4b8420b120cd"
    ) {
      status
      history(first: 10) {
        nodes {
          version
          status
          modifiedBy
          modified
        }
      }
    }
  }
}
```

```json
{
  "status": "DELETED",
  "history": {
    "nodes": [
      {
        "version": 2,
        "status": "DELETED",
        "modifiedBy": "arn:aws:iam::000000000000:user/twisp",
        "modified": "2026-10-11T17:55:38.360631Z"
      },
      {
        "version": 1,
        "status": "ACTIVE",
        "modifiedBy": "arn:twisp:database:::action/2afe6c28-d18d-4f3e-801d-352740eb8925",
        "modified": "2026-10-11T17:54:25.103494Z"
      }
    ]
  }
}
```

Running `ForgetFridays` again returns the record unchanged.

## 9. Confirm the agent forgot

Update the north journal once more to run turn 3:

```graphql
mutation UpdateNorthJournalAgain {
  updateJournal(
    id: "c131e037-1afb-4f10-ba38-ee4956458f79"
    input: { description: "Weekly payouts, reviewed again" }
  ) {
    journalId
    version
  }
}
```

In turn 3's transcript, the same `search_memory` call now returns only the two remaining records:

```json
{
  "memories": [
    { "id": "3c41d8a2-8d97-5263-b7a0-ff103bb6cf17", "summary": "The north journal was opened for weekly payouts.", "kind": "event", "scope": "session", "score": 1.0, ... },
    { "id": "ede55d89-f542-590f-8f6d-ddcf25947ce6", "summary": "Report payout amounts in USD with two decimals.", "kind": "guidance", "scope": "agent", "pinned": true, "score": 0.5903, ... }
  ]
}
```

## 10. Optional: extract with a Claude model

The script stood in for the review after each turn. With a real model, the agent decides for itself what is worth remembering. `harness.memory.model` names the model that reviews; the agent's own turns can stay on the fixture. This step calls a Claude model through the `anthropic` provider, so your local environment needs an Anthropic API key.

This agent has no session, so it reads and writes agent scope only. It runs once for a journal named `agent-memory-west`, and its scripted reply restates the journal's description:

```graphql
mutation CreateExtractingAgent {
  actions {
    createAction(
      id: "a04b1b0e-2a3e-4d7c-9a4f-5a3c1d2e7b61"
      input: {
        name: "agent-memory-extractor"
        kind: "agent"
        agent: { prompt: "You keep notes about payout journals." }
        triggers: [
          { event: "event.type == 'journal.inserted' && event.data.name == 'agent-memory-west'" }
        ]
        config: {
          runtime: "strands"
          harness: {
            memory: {
              enabled: true
              scope: { read: ["agent"], write: ["agent"] }
              model: { provider: "anthropic", id: "claude-haiku-5-5" }
            }
          }
          model: {
            provider: "fixture"
            script: [
              {
                when: "purpose == 'turn'"
                responses: [
                  [
                    {
                      text: "Noted: this journal settles payouts on the 1st and 15th, and disputes go to the treasury desk."
                    }
                  ]
                ]
              }
            ]
          }
        }
      }
    ) {
      actionId
      status
      pending
    }
  }
}
```

Poll `ReadMemoryAgent` with this agent's ID until it is `ACTIVE`, then create the journal:

```graphql
mutation OpenWestJournal {
  createJournal(
    input: {
      journalId: "6a0f6f2e-0d7e-4c4b-8a59-2f3c2b9e4d10"
      name: "agent-memory-west"
      description: "Settles payouts on the 1st and 15th. Disputes go to the treasury desk."
    }
  ) {
    journalId
  }
}
```

After the run, list its memory:

```graphql
query ExtractedMemories {
  actions {
    action(id: "a04b1b0e-2a3e-4d7c-9a4f-5a3c1d2e7b61") {
      memories(first: 10) {
        nodes {
          summary
          content
          kind
          scope
          source
        }
      }
    }
  }
}
```

The model's wording varies from run to run. One run kept a single fact, with a summary to act on and the evidence in `content`:

```json
{
  "nodes": [
    {
      "summary": "Journal 6a0f6f2e-0d7e-4c4b-8a59-2f3c2b9e4d10 (name agent-memory-west) settles payouts on the 1st and 15th of each month; disputes go to the treasury desk.",
      "content": "Recorded from a journal.inserted event at 2026-10-11T17:56:30Z. The journal description reads: \"Settles payouts on the 1st and 15th. Disputes go to the treasury desk.\"",
      "kind": "fact",
      "scope": "AGENT",
      "source": "EXTRACTION"
    }
  ]
}
```

In the run's transcript, the `model.request` with `"purpose": "memory"` names `claude-haiku-5-5`, and its `usage` event counts real tokens, while the turn's own calls name the `fixture` provider.

## 11. Clean up

Delete the agents so later journals named `agent-memory-*` start no sessions:

```graphql
mutation DeleteMemoryAgents {
  actions {
    notes: deleteAction(id: "2afe6c28-d18d-4f3e-801d-352740eb8925") {
      status
      pending
    }
    extractor: deleteAction(id: "a04b1b0e-2a3e-4d7c-9a4f-5a3c1d2e7b61") {
      status
      pending
    }
  }
}
```

Each reports `pending: DELETED`. Deleting an agent is a soft delete: its sessions, executions, and memory records stay readable.

## Troubleshooting

| Symptom                                              | Check                                                                                                                                                   |
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| No session or turn appears                           | The agent must be `ACTIVE` before the journal is created. Create a journal with a fresh ID and the same name.                                           |
| `createJournal` fails with a unique constraint error | The journal ID exists from an earlier run. Choose a fresh ID; keep the name, which the script matches.                                                  |
| Turn 1 of the north session saves nothing            | The journal's name must be exactly `agent-memory-north`: the script's `when` matches it.                                                                |
| `memories` is empty right after a turn               | A record appears shortly after its turn commits. Run the query again.                                                                                   |
| An earlier run's agent also runs on your journals    | It is still active and matches the same journal names. Delete it as in step 11. Its memory is its own: the new agent never sees it.                     |
| A turn fails with `fixture_exhausted`                | A model call found no response left in its entry. Each entry's position restarts every turn; check that the `memory` and `outcome` entries are present. |
| The optional step's run fails with a model error     | The `anthropic` provider needs an Anthropic API key configured in the local environment. The rest of the tutorial calls no model.                       |

Read [Memory](https://www.twisp.com/docs/actions/memory.md) for how to choose scopes, extraction, and pins, and the [memory reference](https://www.twisp.com/docs/reference/actions/memory.md#agent-memory) for every setting, tool, and record field. [Session history](https://www.twisp.com/docs/reference/actions/memory.md#session-history) covers `harness.history`, and [Sessions](https://www.twisp.com/docs/tutorials/actions/sessions.md) walks through session turns in depth.
