Livebook: Compute 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_compute_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()) |> IO.inspect(label: :db_result)
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
In this example, we will create a simple graph with input and compute nodes and play with them.
This tiny tutorial will demonstrate:
- defining a graph
- starting an execution of the graph
- setting values on the execution
- reading values from an execution
- reloading an execution “after a crash” and continuing, as if nothing happened
- a reactive computation getting unblocked when all of its upstream dependencies are provided
- visualizing and introspecting an execution
Define the Graph
import Journey.Node
graph = Journey.new_graph(
"Onboarding",
"v1",
[
input(:name),
input(:email_address),
compute(
:greeting,
[:name, :email_address],
fn values ->
welcome = "Welcome, #{values.name} at #{values.email_address}"
IO.puts(welcome)
{:ok, welcome}
end
)
]
); :ok
:ok
Note that the computation is defined to be unblocked when both inputs, [:name, :email_address], are provided. It will be blocked until then.
Visualize the graph:
graph
|> Journey.Tools.generate_mermaid_graph()
|> Kino.Mermaid.new()
graph TD
%% Graph
subgraph Graph["🧩 'Onboarding', version v1"]
execution_id[execution_id]
last_updated_at[last_updated_at]
name[name]
email_address[email_address]
greeting[["greeting
(anonymous fn)"]]
name --> greeting
email_address --> greeting
end
%% Styling
classDef defaultNode fill:#f8f9fa,stroke:#495057,stroke-width:2px,color:#000000
%% Apply styles to nodes
class execution_id,last_updated_at,name,email_address,greeting defaultNode
Start an Execution
execution = Journey.start(graph); :ok
:ok
Peek into the Execution
We can examine the state of an execution from a few different angles – a diagram, values, and detailed introspection.
Diagram
execution.id
|> Journey.Tools.generate_mermaid_execution()
|> Kino.Mermaid.new()
graph TD
%% Graph
subgraph Graph["🧩 'Onboarding', version v1, EXECZBJVTV467G09Y57076VM"]
execution_id["✅ execution_id"]
last_updated_at["✅ last_updated_at"]
name["⬜ name"]
email_address["⬜ email_address"]
greeting[["🚫 greeting
(anonymous fn)"]]
name --> greeting
email_address --> greeting
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 last_updated_at,execution_id setNode
class greeting,email_address,name neutralNode
Values
Journey.values_all(execution)
%{
name: :not_set,
last_updated_at: {:set, 1776222169},
execution_id: {:set, "EXECZBJVTV467G09Y57076VM"},
email_address: :not_set,
greeting: :not_set
}
Detailed Introspection
Journey.Tools.introspect(execution.id) |> IO.puts()
Execution summary:
- ID: 'EXECZBJVTV467G09Y57076VM'
- Graph: 'Onboarding' | 'v1'
- Archived at: not archived
- Created at: 2026-04-15 03:02:49Z UTC | 0 seconds ago
- Last updated at: 2026-04-15 03:02:49Z UTC | 0 seconds ago
- Duration: 0 seconds
- Revision: 0
- # of Values: 2 (set) / 5 (total)
- # of Computations: 1
Values:
- Set:
- execution_id: 'EXECZBJVTV467G09Y57076VM' | :input
set at 2026-04-15 03:02:49Z | rev: 0
- last_updated_at: '1776222169' | :input
set at 2026-04-15 03:02:49Z | rev: 0
- Not set:
- email_address: | :input
- greeting: | :compute
- name: | :input
Computations:
- Completed:
- Outstanding:
- greeting: ⬜ :not_set (not yet attempted) | :compute
:and
├─ 🛑 :name | &provided?/1
└─ 🛑 :email_address | &provided?/1
:ok
:name Is Set, :greeting Is Still Blocked
execution =
execution
|> Journey.set(:name, "Luigi"); :ok
:ok
Journey.values(execution)
%{name: "Luigi", last_updated_at: 1776222169, execution_id: "EXECZBJVTV467G09Y57076VM"}
Notice that :greeting is still blocked, waiting for :email_address:
execution.id
|> Journey.Tools.generate_mermaid_execution()
|> Kino.Mermaid.new()
graph TD
%% Graph
subgraph Graph["🧩 'Onboarding', version v1, EXECZBJVTV467G09Y57076VM"]
execution_id["✅ execution_id"]
last_updated_at["✅ last_updated_at"]
name["✅ name"]
email_address["⬜ email_address"]
greeting[["🚫 greeting
(anonymous fn)"]]
name --> greeting
email_address --> greeting
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 name,last_updated_at,execution_id setNode
class greeting,email_address neutralNode
Journey.Tools.introspect(execution.id) |> IO.puts()
Execution summary:
- ID: 'EXECZBJVTV467G09Y57076VM'
- Graph: 'Onboarding' | 'v1'
- Archived at: not archived
- Created at: 2026-04-15 03:02:49Z UTC | 0 seconds ago
- Last updated at: 2026-04-15 03:02:49Z UTC | 0 seconds ago
- Duration: 0 seconds
- Revision: 1
- # of Values: 3 (set) / 5 (total)
- # of Computations: 1
Values:
- Set:
- last_updated_at: '1776222169' | :input
set at 2026-04-15 03:02:49Z | rev: 1
- name: '"Luigi"' | :input
set at 2026-04-15 03:02:49Z | rev: 1
- execution_id: 'EXECZBJVTV467G09Y57076VM' | :input
set at 2026-04-15 03:02:49Z | rev: 0
- Not set:
- email_address: | :input
- greeting: | :compute
Computations:
- Completed:
- Outstanding:
- greeting: ⬜ :not_set (not yet attempted) | :compute
:and
├─ ✅ :name | &provided?/1 | rev 1
└─ 🛑 :email_address | &provided?/1
:ok
Reload the Execution After a Crash and Continue
As long as you have the ID of an execution, you can load it and continue as if nothing happened – even if the infrastructure suffered an outage and everything was down for a week.
execution_id = execution.id
"EXECZBJVTV467G09Y57076VM"
execution = Journey.load(execution_id); :ok
:ok
Note that the :name value that we set before “the crash” is still here, even after the execution was reloaded.
This is because as soon as an execution value is set or computed, it is persisted in PostgreSQL.
Journey.values(execution)
%{name: "Luigi", last_updated_at: 1776222169, execution_id: "EXECZBJVTV467G09Y57076VM"}
Set :email_address, Watch :greeting Become Unblocked
At this point, name is set, but greeting is still blocked – it still needs email_address. As soon as we set email_address, greeting will compute its value.
execution =
execution
|> Journey.set(:email_address, "luigi@example.com")
{:ok, value, _revision} = Journey.get(execution, :greeting, wait: :any); value
Welcome, Luigi at luigi@example.com
"Welcome, Luigi at luigi@example.com"
Note that the string appears twice – one is the IO.puts from the :greeting compute function; and the other is the cell’s return value.
Journey.values_all(execution)
%{
name: {:set, "Luigi"},
last_updated_at: {:set, 1776222169},
execution_id: {:set, "EXECZBJVTV467G09Y57076VM"},
email_address: {:set, "luigi@example.com"},
greeting: {:set, "Welcome, Luigi at luigi@example.com"}
}
execution.id
|> Journey.Tools.generate_mermaid_execution()
|> Kino.Mermaid.new()
graph TD
%% Graph
subgraph Graph["🧩 'Onboarding', version v1, EXECZBJVTV467G09Y57076VM"]
execution_id["✅ execution_id"]
last_updated_at["✅ last_updated_at"]
name["✅ name"]
email_address["✅ email_address"]
greeting[["✅ greeting
(anonymous fn)"]]
name --> greeting
email_address --> greeting
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 greeting,email_address,name,last_updated_at,execution_id setNode
Summary
In this Livebook, we played with Journey’s basic functionality: defining and executing reactive graphs with input and compute nodes.
More specifically:
-
We defined a graph with two input nodes and one self-computing node. We then started an execution of the graph, and set a value for
:name. Then we set another value for:email_address, and watched the greeting computation kick off and compute the greeting. -
In the process, we reloaded the execution after an imagined infrastructure crash, and continued with the execution as if nothing happened.
-
We fetched the execution’s values with
Journey.values()andJourney.values_all(). -
We fetched a particular value of
:greeting, waiting for it to become available, withJourney.get(). -
We visualized the graph (
Journey.Tools.generate_mermaid_graph()) and the execution (Journey.Tools.generate_mermaid_execution()), at its various stages. -
We introspected the execution’s current state with
Journey.Tools.introspect()– what values are set, which values are not set. Has the computation completed? If not, what is it blocking on?