---
title: Persistent memory for Jido agents, on your own machine
slug: semurg-memory-for-jido
date: 2026-10-25
author: Semurg
section: how-to
tags: [jido, elixir, http, agents, memory, how-to]
summary: Give a Jido agent a memory that outlives the session, on your own disk, as Jido actions that call the node's HTTP memory routes, with the exact program our tests run.
status: published
covers: [route:POST /v1/memory/remember, route:POST /v1/memory/recall]
constants: [24.04, 1.18]
---
> Last checked on r11.132.20261027.a274a28 (27 Oct 2026): every command below ran on a fresh node of that release. The current release is r11.135.20261028.2ff2348 (28 Oct 2026), and the commands have not been re-checked on it yet.

> Tested end to end with `jido 2.4.0`, `jido_action 2.4.0` and `req 0.7.5`. The release it ran on is named in the checked line at the top of this page.

A Jido agent forgets what it learned when its process ends. This guide gives it a memory that outlives the process: a Semurg node on your
own machine, where every fact stays on your disk and comes back cited to where it was said. Jido is not an MCP client here: its own MCP
package is deprecated, so the memory calls are Jido actions that post to the node's HTTP memory routes, the same calls the MCP tools make.

> Diagram jido-memory: Your Jido actions post a fact to the node's HTTP memory routes, one request per call; your node keeps it in the scope you named, on your own disk; a later run, another agent or an MCP client on the same scope reads it back, cited.

## What you need

- An Intel or AMD x86_64 machine with Ubuntu 24.04, on Linux directly or on Windows through WSL2. One node is free, with no licence key.
- Elixir 1.18 with Erlang/OTP, installed the way you prefer. We test in the official `elixir:1.18-slim` container.

## Step one: install a node

On Ubuntu 24.04 (x86_64), with sudo:

```bash
curl -fsSL https://one.semurg.io/install | sudo bash
```

The [install page](/install) lists what the installer needs and what it changes. The installer ends by printing your API token, and the
node keeps a copy. Put it in a file only you can read, and have every new terminal load it:

```bash
mkdir -p ~/.config/semurg
(umask 077; sudo grep '^SEMURG_API_TOKEN=' /etc/semurg/semurg.env | cut -d= -f2- > ~/.config/semurg/token)
echo 'export SEMURG_API_TOKEN="$(cat ~/.config/semurg/token)"' >> ~/.bashrc
export SEMURG_API_TOKEN="$(cat ~/.config/semurg/token)"
```

## Step two: the packages

There is nothing to install by hand. The program's first line fetches and compiles Jido, its actions and the Req HTTP client on the first
run:

```elixir
Mix.install([{:jido, "~> 2.0"}, {:jido_action, "~> 2.0"}, {:req, "~> 0.5"}])
```

On a fresh Elixir install, fetch the package tools once first, as our test setup does:

```bash
mix local.hex --force
mix local.rebar --force
```

## Step three: connect

Save the program below as `snippet.exs`. It is the program our tests run as written, shown here from the [agents page](/agents#jido)
rather than copied, so what you read is always the file that ran.

```elixir
# Run: elixir snippet.exs   (SEMURG_API_TOKEN must be set in the environment)
Mix.install([{:jido, "~> 2.0"}, {:jido_action, "~> 2.0"}, {:req, "~> 0.5"}])

defmodule Semurg.Node do
  @base System.get_env("SEMURG_MCP_URL", "http://127.0.0.1:4000/v1/mcp") |> String.replace_suffix("/v1/mcp", "")
  @scope System.get_env("SEMURG_MCP_SCOPE", "default")
  @token System.fetch_env!("SEMURG_API_TOKEN")

  # the HTTP memory routes read the scope from the body
  def post(verb, body) do
    {:ok, %Req.Response{status: 200, body: answer}} =
      Req.post(@base <> "/v1/memory/" <> verb, json: Map.put(body, :scope, @scope), headers: [{"authorization", "Bearer " <> @token}], retry: false)

    {:ok, %{answer: answer}}
  end
end

defmodule Semurg.Remember do
  use Jido.Action, name: "memory_remember", description: "Store facts in shared agent memory", schema: [facts: [type: :map, required: true]]
  @impl true
  def run(params, _context), do: Semurg.Node.post("remember", Map.take(params, [:facts]))
end

defmodule Semurg.Recall do
  use Jido.Action, name: "memory_recall", description: "Recall what is stored about ONE entity", schema: [subject: [type: :string, required: true]]
  @impl true
  def run(params, _context), do: Semurg.Node.post("recall", Map.take(params, [:subject]))
end

facts = %{entities: [%{name: "Ada", type: "person"}, %{name: "Acme", type: "organisation"}], relations: [%{source: "Ada", target: "Acme", type: "WORKS_AT"}]}
{:ok, _} = Jido.Exec.run(Semurg.Remember, %{facts: facts})
IO.inspect(Jido.Exec.run(Semurg.Recall, %{subject: "Ada"}), limit: :infinity, printable_limit: :infinity)
```

*Ran: this exact file, end to end against a fresh node on release r11.132.20261027.a274a28, with jido 2.4.0, jido_action 2.4.0, req 0.7.5. Install: `Mix.install([{:jido, "~> 2.0"}, {:jido_action, "~> 2.0"}, {:req, "~> 0.5"}])`. Its run record: https://one.semurg.io/agents#jido*

It reads your token from `SEMURG_API_TOKEN`, the node's address from `SEMURG_MCP_URL` (without it, `http://127.0.0.1:4000/v1/mcp`, from
which it takes the node's base address) and the scope from `SEMURG_MCP_SCOPE` (without it, the scope named `default`). It defines two Jido
actions, `memory_remember` and `memory_recall`, that post to `/v1/memory/remember` and `/v1/memory/recall` with the token and the scope in
the request body. It runs the first with two facts (Ada works at Acme) and prints what the second returns for Ada.

## Step four: run it

Give the program a scope of its own, so it never writes into a memory you use for real work:

```bash
export SEMURG_MCP_SCOPE=try-jido
elixir snippet.exs
```

The first run is slower, because it fetches and compiles the packages. The program then inspects the recall action's result, an `:ok`
tuple carrying the node's answer for Ada: `state` set to `answered` and the `WORKS_AT` fact that links Ada to Acme, each item cited.
Storing the same facts again changes nothing, so it is safe to run twice.

Now read the same fact with plain curl. What one client writes, any other client on the same node and scope reads, over HTTP or MCP:

```bash
curl -sS http://127.0.0.1:4000/v1/memory/recall \
  -H "Authorization: Bearer $SEMURG_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"scope":"try-jido","subject":"Ada"}'
```

## The memory calls for Jido

The program wraps two of the memory calls as actions. Each of the others is one more `Jido.Action` of the same shape: a POST of a JSON
body with the scope, except the status call, which is a GET. Our test runs the actions directly with `Jido.Exec.run/2`, without a model.

- `memory_status` (`GET /v1/memory/status`): reports whether shared memory is on for this node and which of your scopes hold facts.
- `memory_remember` (`POST /v1/memory/remember`): stores entities, the relationships between them and the passages they came from, or plain text, with an optional source label and date.
- `memory_recall` (`POST /v1/memory/recall`): recalls what is stored about one named entity, its own facts first, each item cited.
- `memory_relate` (`POST /v1/memory/relate`): adds one typed relationship between two entities in a scope, creating an end that is not stored yet.
- `memory_context` (`POST /v1/memory/context`): answers a question in plain words with cited items and a verdict on whether the store covers it.
- `memory_document` (`POST /v1/memory/document`): takes a whole document (PDF with a text layer, Word, HTML or plain text) and stores it as cited passages in a scope.
- `memory_forget` (`POST /v1/memory/forget`): deletes one thing per call: an entity and everything that names it, one fact, one document, every document under a source label, or the whole scope. It is unreachable at once and not yet wiped from the disk.
- `memory_export` (`POST /v1/memory/export`): reads everything a scope holds, one page per call: send each reply's `next` back as `cursor` until it is empty. The records are the entities, the facts with their dated sources and whether each is current or ended, and each stored document once; a forgotten passage is never exported.
- `memory_import` (`POST /v1/memory/import`): stores the records `memory_export` produced into any scope, through the same writes `memory_remember` makes. A page sent twice stores nothing twice, and the reply names every record it skipped.

Every answer says its `state`: `answered`, `nothing_stored` (nothing stored matched, so the agent should say it does not know) or `could_not_judge`.

## Troubleshooting

- An error that the environment variable `SEMURG_API_TOKEN` is not set: run the last line of step one, or open a new terminal.
- A `MatchError` on a `Req.Response`: the node answered with a status other than success. A `401` means the token is wrong; a `422` names what the node refused in the call.
- Set `SEMURG_MCP_URL` to the node's MCP address, ending in `/v1/mcp`, even though Jido uses the HTTP routes: the program takes the node's base address from it.
- `Mix.install` cannot fetch the packages on a fresh Elixir: run the two `mix local` lines of step two once.
- Every call is its own request, so a node restart (an update restarts it) leaves no session to lose: the next call simply works. For a node that is up but stalled, set the per-call timeout your framework offers.

Use `127.0.0.1` rather than `localhost`: on Ubuntu `localhost` can resolve to the IPv6 address first, and the node listens on IPv4 loopback
only. To reach a node on another machine, forward its port with `ssh -N -L 4000:127.0.0.1:4000 you@your-node`.

## Next

- The [agents page](/agents): every framework program, with its versions and the release it last ran on in our tests.
- [Give your agent persistent memory with MCP in five minutes](/blog/agent-memory-mcp-five-minutes): the same memory calls over plain HTTP, step by step.
- The [develop page](/develop): every HTTP memory route in full, and the limits each call refuses by name.
- If a step does not work, tell us on the [contact page](/contact), with the reply you got.

*This post was written with AI assistance. Every result in it is checked against a published release or record before the post goes out.*
