“historian” nodes
# [Optional] Setting Build Key, see https://gojourney.dev/your_keys
# (Using "Journey Livebook Demo" build key)
System.put_env("JOURNEY_BUILD_KEY", "B27AXHMERm2Z6ehZhL49v")
Mix.install(
[
{:ecto_sql, "~> 3.13"},
{:postgrex, "~> 0.22"},
{:jason, "~> 1.4"},
{:journey, "~> 0.10"},
{:kino, "~> 0.19"}
],
start_applications: false
)
Application.put_env(:journey, :log_level, :warning)
# This livebook requires a PostgreSQL database.
# If you don't have one running, you can start one with Docker:
# docker run --rm --name postgres -p 5432:5432 -e POSTGRES_PASSWORD=postgres -d postgres:16
# Update this configuration to point to your database server
Application.put_env(:journey, Journey.Repo,
database: "journey_historian_nodes",
username: "postgres",
password: "postgres",
hostname: "localhost",
log: false,
port: 5432
)
Application.put_env(:journey, :ecto_repos, [Journey.Repo])
Journey.Repo.__adapter__().storage_up(Journey.Repo.config())
Application.loaded_applications()
|> Enum.map(fn {app, _, _} -> app end)
|> Enum.each(&Application.ensure_all_started/1)
DB Setup
This livebook requires a PostgreSQL database. If you don’t have one running, you can start one with Docker:
docker run --rm --name postgres -p 5432:5432 -e POSTGRES_PASSWORD=postgres -d postgres:16
What We’ll Cover
This tutorial focuses on the historian node type.
A historian node reactively records changes to the nodes it watches. Every time a watched node gets a new value, the historian appends an entry to its history list. No function is needed — you just declare what to watch.
We’ll build a simple price tracker: an input node for the current price, and a historian that records every price change.
This tutorial will:
-
define a graph with an
inputnode for:current_priceand ahistoriannode that watches it, - update the price and inspect the history,
- update the price two more times (also capturing some metadata connected to the 3rd update), and watch the history accumulate,
- visualize and introspect the execution.
Define the Graph
import Journey.Node
graph = Journey.new_graph(
"Price Tracker",
"v1",
[
input(:current_price),
historian(:price_history, [:current_price])
]
); :ok
:ok
Two nodes: :current_price is an input, and :price_history is a historian that watches it. Every time :current_price gets a new value, the historian appends an entry to its history.
Visualize the graph:
graph
|> Journey.Tools.generate_mermaid_graph()
|> Kino.Mermaid.new()
graph TD
%% Graph
subgraph Graph["🧩 'Price Tracker', version v1"]
execution_id[execution_id]
last_updated_at[last_updated_at]
current_price[current_price]
price_history[["price_history
(anonymous fn)"]]
current_price --> price_history
end
%% Styling
classDef defaultNode fill:#f8f9fa,stroke:#495057,stroke-width:2px,color:#000000
%% Apply styles to nodes
class execution_id,last_updated_at,current_price,price_history defaultNode
Start an Execution
execution = Journey.start(graph); :ok
:ok
First Price Update
execution = Journey.set(execution, :current_price, 100)
{:ok, history, revision} = Journey.get(execution, :price_history, wait: :any)
history
[
%{
"metadata" => nil,
"node" => "current_price",
"revision" => 1,
"timestamp" => 1776282255,
"value" => 100
}
]
The historian has one entry. Each entry is a map with "value", "node", "timestamp", and "revision" — telling you what changed, which node it was, and when.
Second Price Update
execution = Journey.set(execution, :current_price, 105)
{:ok, history, revision} = Journey.get(execution, :price_history, wait: {:newer_than, revision})
history
[
%{
"metadata" => nil,
"node" => "current_price",
"revision" => 4,
"timestamp" => 1776282256,
"value" => 105
},
%{
"metadata" => nil,
"node" => "current_price",
"revision" => 1,
"timestamp" => 1776282255,
"value" => 100
}
]
The history now has two entries, newest first. The historian accumulated this automatically — we didn’t write any logging code.
Third Price Update
In this update, Journey.set() will supply optional metadata: – the name of the person who requested the update.
The metadata will be captured by the historian.
execution = Journey.set(execution, :current_price, 98, metadata: %{"author" => "mario"})
{:ok, history, revision} = Journey.get(execution, :price_history, wait: {:newer_than, revision})
history
[
%{
"metadata" => %{"author" => "mario"},
"node" => "current_price",
"revision" => 7,
"timestamp" => 1776282256,
"value" => 98
},
%{
"metadata" => nil,
"node" => "current_price",
"revision" => 4,
"timestamp" => 1776282256,
"value" => 105
},
%{
"metadata" => nil,
"node" => "current_price",
"revision" => 1,
"timestamp" => 1776282255,
"value" => 100
}
]
Three entries, newest first. The pattern is clear: every price change is recorded, building a complete history of how the price moved over time.
Diagram
execution.id
|> Journey.Tools.generate_mermaid_execution()
|> Kino.Mermaid.new()
graph TD
%% Graph
subgraph Graph["🧩 'Price Tracker', version v1, EXEC7BTTZ34DJ8XT70RYL07M"]
execution_id["✅ execution_id"]
last_updated_at["✅ last_updated_at"]
current_price["✅ current_price"]
price_history[["✅ price_history
(anonymous fn)"]]
current_price --> price_history
end
%% Styling
classDef setNode fill:#e1f5fe,stroke:#01579b,stroke-width:2px,color:#000000
classDef computingNode fill:#fff8e1,stroke:#f57f17,stroke-width:2px,color:#000000
classDef errorNode fill:#f8bbd0,stroke:#b71c1c,stroke-width:2px,color:#000000
classDef neutralNode fill:#f8f9fa,stroke:#495057,stroke-width:2px,color:#000000
%% Apply styles to nodes
class price_history,current_price,last_updated_at,execution_id setNode
Detailed Introspection
Journey.Tools.introspect(execution.id) |> IO.puts()
Execution summary:
- ID: 'EXEC7BTTZ34DJ8XT70RYL07M'
- Graph: 'Price Tracker' | 'v1'
- Archived at: not archived
- Created at: 2026-04-15 19:44:15Z UTC | 1 seconds ago
- Last updated at: 2026-04-15 19:44:16Z UTC | 0 seconds ago
- Duration: 1 seconds
- Revision: 9
- # of Values: 4 (set) / 4 (total)
- # of Computations: 3
Values:
- Set:
- last_updated_at: '1776282256' | :input
set at 2026-04-15 19:44:16Z | rev: 9
- price_history: '[%{"metadata" => %{"author" => "mario"}, "node" => "current_price", "revision" => 7, "timestamp" => 1776282256, "value" => 98}, %{"metadata" => nil, "node" => "current_price", "revision" => 4, "timestamp" => 1776282256, "value" => 105}, %{"metadata" => nil, "node" => "current_price", "revision" => 1, "timestamp" => 1776282255, "value" => 100}]' | :historian
computed at 2026-04-15 19:44:16Z | rev: 9
- current_price: '98' | :input
set at 2026-04-15 19:44:16Z | rev: 7
- execution_id: 'EXEC7BTTZ34DJ8XT70RYL07M' | :input
set at 2026-04-15 19:44:15Z | rev: 0
- Not set:
Computations:
- Completed:
- :price_history (CMP4H4G7B64441VRJ054R8T): ✅ :success | :historian | rev 9
started: 2026-04-15 19:44:16Z | completed: 2026-04-15 19:44:16Z (0s)
inputs used:
:current_price (rev 7)
- :price_history (CMPLVJL4D5HA44RJRRME43J): ✅ :success | :historian | rev 6
started: 2026-04-15 19:44:16Z | completed: 2026-04-15 19:44:16Z (0s)
inputs used:
:current_price (rev 4)
- :price_history (CMPA3G3420TD9625AEZRAH8): ✅ :success | :historian | rev 3
started: 2026-04-15 19:44:15Z | completed: 2026-04-15 19:44:15Z (0s)
inputs used:
:current_price (rev 1)
- Outstanding:
:ok
Going Further: max_entries
By default, a historian keeps up to 1000 entries. When the limit is reached, the oldest entries are dropped. You can customize this with the max_entries option:
# Keep only the 50 most recent price updates
historian(:price_history, [:current_price], max_entries: 50)
# Keep unlimited history (use with caution)
historian(:audit_log, [:audit_event], max_entries: nil)
See the historian/3 documentation for more examples.
Summary
In this Livebook, we saw how historian nodes reactively record changes to the nodes they watch.
We built a price tracker with just two nodes — an input and a historian — and watched the history grow as we updated the price.
Key takeaways:
-
A
historiannode records changes automatically — you declare what to watch, and it keeps the log. -
Each history entry captures the value, the node name, a timestamp, and optional metadata.
-
A historian can watch multiple nodes, recording the history of changes for every one of them. Ours watched
:current_price. -
History is stored newest-first.
-
max_entriescontrols how many entries to keep (default: 1000). Set tonilfor unlimited. -
Adding a historian doesn’t affect the rest of the graph — it’s a passive observer on its own branch.