Powered by AppSignal & Oban Pro

Livebook: Compute Nodes

lib/examples/compute.livemd

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:

  1. defining a graph
  2. starting an execution of the graph
  3. setting values on the execution
  4. reading values from an execution
  5. reloading an execution “after a crash” and continuing, as if nothing happened
  6. a reactive computation getting unblocked when all of its upstream dependencies are provided
  7. 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() and Journey.values_all().

  • We fetched a particular value of :greeting, waiting for it to become available, with Journey.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?