Skip to main content

lib/examples/mutate.livemd

<!-- livebook:{"persist_outputs":true} -->

# "mutate" nodes

```elixir
# [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:

```bash
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:

1. define a graph that includes an `input` node with sensitive data (`:ssn`), a computation (`:credit_score`), and a `mutate` node (`:redact_ssn`) targeting `:ssn`,
2. start an execution of the graph,
3. set `:ssn`, watch `:credit_score` compute, and watch the `:redact_ssn` node mutate `:ssn`, redacting its sensitive data,
4. read values from the execution,
5. visualize and introspect the execution.

## Define the Graph

```elixir
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, "<redacted>"} end,
      mutates: :ssn
    )
  ]
); :ok
```

<!-- livebook:{"output":true} -->

```
: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 `"<redacted>"`.

<!-- livebook:{"break_markdown":true} -->

Visualize the graph:

```elixir
graph
|> Journey.Tools.generate_mermaid_graph()
|> Kino.Mermaid.new()
```

<!-- livebook:{"output":true} -->

```mermaid
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<br/>(anonymous fn)"]]
        redact_ssn[["redact_ssn<br/>(anonymous fn)<br/>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

```elixir
execution = Journey.start(graph); :ok
```

<!-- livebook:{"output":true} -->

```
:ok
```

## Set Inputs

```elixir
execution =
  execution
  |> Journey.set(:name, "Luigi")
  |> Journey.set(:ssn, "123-45-6789"); :ok
```

<!-- livebook:{"output":true} -->

```
:ok
```

Setting `:name` and `:ssn` unblocks `:credit_score`, which in turn unblocks `:redact_ssn`, which overwrites `:ssn`.

## Watch It Unfold

```elixir
{:ok, score, _revision} = Journey.get(execution, :credit_score, wait: :any)
score
```

<!-- livebook:{"output":true} -->

```
redacting :ssn
```

<!-- livebook:{"output":true} -->

```
728
```

The credit score was computed using the original `:ssn`, and `:redact_ssn`'s function provided the new value for `:ssn`:

```elixir
Journey.values(execution)
```

<!-- livebook:{"output":true} -->

```
%{
  name: "Luigi",
  ssn: "<redacted>",
  credit_score: 728,
  last_updated_at: 1776919639,
  execution_id: "EXECZ3D6J01R3T00TZ47L1JR",
  redact_ssn: "updated :ssn"
}
```

The value of `:ssn` has been replaced with `"<redacted>"`.

<!-- livebook:{"break_markdown":true} -->

### Diagram

```elixir
execution.id
|> Journey.Tools.generate_mermaid_execution()
|> Kino.Mermaid.new()
```

<!-- livebook:{"output":true} -->

```mermaid
graph TD
    %% Graph
    subgraph Graph["🧩 'Credit Check', version v1, EXECZ3D6J01R3T00TZ47L1JR"]
        execution_id["✅ execution_id"]
        last_updated_at["✅ last_updated_at"]
        name["✅ name"]
        ssn["✅ ssn"]
        credit_score[["✅ credit_score<br/>(anonymous fn)"]]
        redact_ssn[["✅ redact_ssn<br/>(anonymous fn)<br/>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

```elixir
Journey.Tools.introspect(execution.id) |> IO.puts()
```

<!-- livebook:{"output":true} -->

```
Execution summary:
- ID: 'EXECZ3D6J01R3T00TZ47L1JR'
- Graph: 'Credit Check' | 'v1'
- Archived at: not archived
- Created at: 2026-04-23 04:47:19Z UTC | 0 seconds ago
- Last updated at: 2026-04-23 04:47:19Z UTC | 0 seconds ago
- Duration: 0 seconds
- Revision: 6
- # of Values: 6 (set) / 6 (total)
- # of Computations: 2

Values:
- Set:
  - last_updated_at: '1776919639' | :input
    set at 2026-04-23 04:47:19Z | rev: 6

  - redact_ssn: '"updated :ssn"' | :mutate
    computed at 2026-04-23 04:47:19Z | rev: 6

  - credit_score: '728' | :compute
    computed at 2026-04-23 04:47:19Z | rev: 4

  - ssn: '"<redacted>"' | :input
    set at 2026-04-23 04:47:19Z | rev: 2

  - name: '"Luigi"' | :input
    set at 2026-04-23 04:47:19Z | rev: 1

  - execution_id: 'EXECZ3D6J01R3T00TZ47L1JR' | :input
    set at 2026-04-23 04:47:19Z | rev: 0


- Not set:
  

Computations:
- Completed:
  - :redact_ssn (CMPT15A7643G8T1117RV36Z): ✅ :success | :mutate | rev 6
    started: 2026-04-23 04:47:19Z | completed: 2026-04-23 04:47:19Z (0s)
    inputs used:
       :credit_score (rev 4)

  - :credit_score (CMPZ94YM19XDBB3RJL3YXTR): ✅ :success | :compute | rev 4
    started: 2026-04-23 04:47:19Z | completed: 2026-04-23 04:47:19Z (0s)
    inputs used:
       :name (rev 1)
       :ssn (rev 2)

- Outstanding:

```

<!-- livebook:{"output":true} -->

```
: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`:

<!-- livebook:{"break_markdown":true} -->

Here is an example:

<!-- livebook:{"break_markdown":true} -->

<!-- livebook:{"force_markdown":true} -->

```elixir
...
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
)
...
```

<!-- livebook:{"break_markdown":true} -->

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 `mutate` node writes to another node (specified by `mutates:`), unlike `compute` which stores its own result.

* Mutate nodes are gated by their upstream dependencies just like `compute` nodes — `:redact_ssn` only fired after `:credit_score` was computed.

* This pattern is useful for PII redaction, cache updates, and any scenario where you need to update an `input` node.