How to call functions from a running Phoenix app

When using Livebook for internal tools, runbooks, or engineering support, a common need is to call code from a running Phoenix app.

This guide shows how to connect your notebook to your Phoenix app and execute remote function calls, both in development and production environments.

Connect to your local Phoenix app

First, start your Phoenix app with a named node and cookie:

$ iex --name my_app@127.0.0.1 --cookie secret -S mix phx.server

Now, create a new notebook, and add a remote execution smart cell:

Set the node and cookie configs to the values you set when starting your Phoenix app:

Now you can write code inside that smart cell, and it will be evaluated in the context of your Phoenix app's node:

Understanding how Livebook leverages distributed Erlang

By default, Livebook starts a new Erlang VM node for each notebook. This is the standalone runtime.

Under the hood, the remote execution smart cell leverages distributed Erlang to call functions from your Phoenix app.

It clusters your notebook's node with your Phoenix app's node, and evaluates the code inside the smart cell in the context of your Phoenix app's node.

graph LR
    subgraph livebook_node["Erlang VM node"]
        A[Livebook]
    end

   subgraph standalone_node["Erlang VM node per notebook"]
        B[code inside the notebook]
    end

   subgraph app_node["Erlang VM node running your Phoenix app"]
        C[Phoenix app]
    end

   A -.-|starts and clusters with| standalone_node
   standalone_node -.-|clusters with| app_node

Connect to your Phoenix app in production

When developing and deploying a Livebook app that integrates with a Phoenix app, you need a way to handle the connection between them during both development and production.

To support both environments, use Livebook secrets to make the connection between your notebook and your Phoenix app configurable.

Set up environment secrets

Create these secrets in your Livebook Teams workspace:

For development:

  • PHOENIX_APP_ENV: Set to dev
  • PHOENIX_APP_COOKIE: Set to your local cookie value (e.g., secret)

For production: Use additional secrets in your Teams workspace deployment group to override these values:

  • PHOENIX_APP_ENV: Set to production
  • PHOENIX_APP_COOKIE: Set to your production cookie value

Create a connection module

Add this module to your notebook to handle environment-specific connections:

defmodule NodeConnection do
  def connect() do
    Node.set_cookie(cookie())

    case Node.connect(target_node()) do
      true -> :ok
      _ -> {:error, "Failed to connect to #{inspect(target_node())}"}
    end
  end

  def cookie() do
    String.to_atom(System.fetch_env!("LB_PHOENIX_APP_COOKIE"))
  end

  def target_node() do
    case System.fetch_env!("LB_PHOENIX_APP_ENV") do
      "dev" ->
        :"my_app@127.0.0.1"
      env when env in ["staging", "production"] ->
        discover_node()
    end
  end

  defp discover_node() do
    # Implementation depends on your deployment platform
    # See platform-specific examples below
  end
end

Using the new NodeConnection module, get the node and cookie values and assign them to variables to be used as configurations in the remote execution smart cell:

my_app_node = NodeConnection.target_node()
my_app_cookie = NodeConnection.cookie()

Now you're ready to use the remote execution smart cell, with the node and cookie being set dynamically:

Discovery of node names in production

In a production environment, you need to programmatically discover your app's node name.

The approach depends on your deployment platform and node naming strategy. Here's an example:

Example: Fly.io node discovery

When deploying to Fly.io, your Phoenix app node is typically named using the RELEASE_NODE environment variable like this:

# rel/env.sh.eex

export RELEASE_NODE="${FLY_APP_NAME}-${FLY_IMAGE_REF##*-}@${FLY_PRIVATE_IP}"

Let's build a solution that uses Fly's API to discover that node name.

  1. Create these additional secrets inside your Teams workspace:
  • FLY_TOKEN: Your Fly.io API token
  • FLY_APP: Your Fly app name
  1. Add this Fly.io discovery module to your notebook:
# Add {:req, "~> 0.5"} to your notebook dependencies

defmodule Fly do
  def discover_node() do
    {:ok, [fly_machine | _]} = machines(fly_app_name())
    ip = fly_machine["private_ip"]
    :"#{fly_app_name()}-#{extract_image_id(fly_machine)}@#{ip}"
  end

  defp machines(fly_app_name) do
    case Req.get(new(), url: "/v1/apps/#{fly_app_name}/machines") do
      {:ok, %Req.Response{status: 200} = response} ->
        {:ok, response.body}
      {:error, reason} ->
        {:error, "Failed to fetch Fly machines: #{inspect(reason)}"}
    end
  end

  defp new() do
    Req.new(
      base_url: "https://api.machines.dev",
      auth: {:bearer, System.fetch_env!("LB_FLY_TOKEN")}
    )
  end

  defp extract_image_id(fly_machine) do
    image_tag = fly_machine["image_ref"]["tag"]
    [image_id] = Regex.run(~r/.*-(.*)/, image_tag, capture: :all_but_first)
    image_id
  end

  defp fly_app_name, do: System.fetch_env!("LB_FLY_APP")
end
  1. Update your NodeConnection module to use Fly discovery:
defmodule NodeConnection do
  # ... previous code ...

  defp discover_node() do
    Fly.discover_node()  # Use Fly.io node discovery
  end
end

Configure your Phoenix app for clustering

Your Phoenix app needs specific configuration to support clustering.

Set a static cookie

Set the RELEASE_COOKIE environment variable on your production machines to ensure a static cookie value across deployments, then restart or redeploy your app.

Use the same value for your PHOENIX_APP_COOKIE Livebook secret.

Learn more about setting the cookie of an Elixir release here.

Enable long node names

Livebook requires long node names. Configure the RELEASE_DISTRIBUTION environment variable inside your app's rel/env.sh.eex like this:

# rel/env.sh.eex

export RELEASE_DISTRIBUTION=name

Conveniences for working with remote code

Livebook provides built-in tools to simplify working with remote code.

Remote execution smart cell

The remote execution smart cell offers several advantages over manual :erpc.call functions:

Built-in connection management

Set the node name and cookie directly in the smart cell.

Code autocomplete

Get autocomplete for functions from your remote Phoenix app.

Multi-line execution

Run multiple lines of code in the remote context.

Variable integration

Reference notebook variables and assign results back to the notebook.

Kino.RPC

For more flexibility, use Kino.RPC directly. This is the same module the remote execution smart cell uses behind the scenes.

Let's say you're building a Livebook app to show user counts by status. Given your Phoenix app has a function MyApp.Users.count_by_status/1, you can call it using the remote execution smart cell:

But what if you want the status variable to come from a form input?

Now we need a way to call the remote function passing an argument that's coming from the form. To do that, we can extract the remote function call into a reusable module.

First, convert your remote execution smart cell to a regular code cell by clicking on the pencil icon:

You'll see that the smart cell was generating this code:

require Kino.RPC
node = my_app_node
Node.set_cookie(node, my_app_cookie)
Kino.RPC.eval_string(node, ~S"MyApp.Users.count_by_status(status)", file: __ENV__.file)

Extract this into a module so you can pass the status value as an argument:

Node.set_cookie(my_app_node, my_app_cookie)

defmodule MyAppRPC.Users do
  require Kino.RPC

  def count_by_status(node, status) do
    Kino.RPC.eval_string(node, ~S"MyApp.Users.count_by_status(status)", file: __ENV__.file)
  end
end

Now you can use this module with your form: