“mutate” 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_mutate_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 mutate node type.
A mutate node is like a compute node, but instead of storing its own value, it overwrites the value of another node.
We’ll illustrate the use of a mutate node with a practical PII redaction scenario: a user-supplied SSN is used to compute the user’s credit score, then automatically redacted.
This tutorial will:
-
define a graph that includes an
inputnode with sensitive data (:ssn), a computation (:credit_score), and amutatenode (:redact_ssn) targeting:ssn, - start an execution of the graph,
-
set
:ssn, watch:credit_scorecompute, and watch the:redact_ssnnode mutate:ssn, redacting its sensitive data, - read values from the execution,
- visualize and introspect the execution.
Define the Graph
import Journey.Node
graph = Journey.new_graph(
"Credit Check",
"v1",
[
input(:name),
input(:ssn),
compute(:credit_score, [:name, :ssn],
fn %{name: name, ssn: ssn} ->
seed = :erlang.phash2({name, ssn})
score = 300 + rem(seed, 551)
{:ok, score}
end
),
mutate(:redact_ssn, [:credit_score],
fn _ -> IO.puts("redacting :ssn"); {:ok, ""} end,
mutates: :ssn
)
]
); :ok
:ok
The :credit_score compute node depends on both :name and :ssn. Once the credit score is computed, the :redact_ssn mutate node fires and overwrites :ssn with "".
Visualize the graph:
graph
|> Journey.Tools.generate_mermaid_graph()
|> Kino.Mermaid.new()
graph TD
%% Graph
subgraph Graph["🧩 'Credit Check', version v1"]
execution_id[execution_id]
last_updated_at[last_updated_at]
name[name]
ssn[ssn]
credit_score[["credit_score
(anonymous fn)"]]
redact_ssn[["redact_ssn
(anonymous fn)
mutates: ssn"]]
name --> credit_score
ssn --> credit_score
credit_score --> redact_ssn
end
%% Styling
classDef defaultNode fill:#f8f9fa,stroke:#495057,stroke-width:2px,color:#000000
%% Apply styles to nodes
class execution_id,last_updated_at,name,ssn,credit_score,redact_ssn defaultNode
Start an Execution
execution = Journey.start(graph); :ok
:ok
Set Inputs
execution =
execution
|> Journey.set(:name, "Luigi")
|> Journey.set(:ssn, "123-45-6789"); :ok
:ok
Setting :name and :ssn unblocks :credit_score, which in turn unblocks :redact_ssn, which overwrites :ssn.
Watch It Unfold
{:ok, score, _revision} = Journey.get(execution, :credit_score, wait: :any)
score
redacting :ssn
728
The credit score was computed using the original :ssn, and :redact_ssn‘s function provided the new value for :ssn:
Journey.values(execution)
%{
name: "Luigi",
ssn: "",
credit_score: 728,
last_updated_at: 1776239756,
execution_id: "EXECXMG9Z4682R8Y8GL609TE",
redact_ssn: "updated :ssn"
}
The value of :ssn has been replaced with "".
Diagram
execution.id
|> Journey.Tools.generate_mermaid_execution()
|> Kino.Mermaid.new()
graph TD
%% Graph
subgraph Graph["🧩 'Credit Check', version v1, EXECXMG9Z4682R8Y8GL609TE"]
execution_id["✅ execution_id"]
last_updated_at["✅ last_updated_at"]
name["✅ name"]
ssn["✅ ssn"]
credit_score[["✅ credit_score
(anonymous fn)"]]
redact_ssn[["✅ redact_ssn
(anonymous fn)
mutates: ssn"]]
name --> credit_score
ssn --> credit_score
credit_score --> redact_ssn
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 redact_ssn,credit_score,ssn,name,last_updated_at,execution_id setNode
Detailed Introspection
Journey.Tools.introspect(execution.id) |> IO.puts()
Execution summary:
- ID: 'EXECXMG9Z4682R8Y8GL609TE'
- Graph: 'Credit Check' | 'v1'
- Archived at: not archived
- Created at: 2026-04-15 07:55:56Z UTC | 0 seconds ago
- Last updated at: 2026-04-15 07:55:56Z UTC | 0 seconds ago
- Duration: 0 seconds
- Revision: 6
- # of Values: 6 (set) / 6 (total)
- # of Computations: 2
Values:
- Set:
- last_updated_at: '1776239756' | :input
set at 2026-04-15 07:55:56Z | rev: 6
- redact_ssn: '"updated :ssn"' | :mutate
computed at 2026-04-15 07:55:56Z | rev: 6
- credit_score: '728' | :compute
computed at 2026-04-15 07:55:56Z | rev: 4
- ssn: '""' | :input
set at 2026-04-15 07:55:56Z | rev: 2
- name: '"Luigi"' | :input
set at 2026-04-15 07:55:56Z | rev: 1
- execution_id: 'EXECXMG9Z4682R8Y8GL609TE' | :input
set at 2026-04-15 07:55:56Z | rev: 0
- Not set:
Computations:
- Completed:
- :redact_ssn (CMP6E5721X6B17MVHAL7T71): ✅ :success | :mutate | rev 6
started: 2026-04-15 07:55:56Z | completed: 2026-04-15 07:55:56Z (0s)
inputs used:
:credit_score (rev 4)
- :credit_score (CMP0A2R1151B01AX0G2R6AE): ✅ :success | :compute | rev 4
started: 2026-04-15 07:55:56Z | completed: 2026-04-15 07:55:56Z (0s)
inputs used:
:name (rev 1)
:ssn (rev 2)
- Outstanding:
:ok
Going Further: update_revision_on_change
By default, a mutation overwrites the target node’s value without triggering downstream recomputation. If you want the mutation to also trigger downstream nodes to recompute, pass update_revision_on_change: true:
Here is an example:
...
input(:cached_price),
mutate(:fetch_current_price, [:polling_schedule],
fn _ -> {:ok, fetch_current_market_price()} end,
mutates: :cached_price,
update_revision_on_change: true
),
compute(
:report_price_update,
[:cached_price],
fn %{cached_price: new_price} -> send_price_update_notification(new_price) end
)
...
See the mutate/4 documentation for a working example.
Summary
In this Livebook, we saw how mutate nodes overwrite another node’s value.
We used this functionality to automatically redact an SSN after it was used to compute a credit score.
Key takeaways:
-
A
mutatenode writes to another node (specified bymutates:), unlikecomputewhich stores its own result. -
Mutate nodes are gated by their upstream dependencies just like
computenodes —:redact_ssnonly fired after:credit_scorewas computed. -
This pattern is useful for PII redaction, cache updates, and any scenario where you need to update an
inputnode.