Elixir and Livebook


Elixir and Livebook


In this notebook, we will explore some unique features when using Elixir and Livebook together, such as inputs, autocompletion, and more.

If you are not familiar with Elixir, there is a fast paced introduction to the language in the Distributed portals with Elixir notebook. For a more structured introduction to the language, see Elixir’s Getting Started guide and the many learning resources available.

Let’s move forward.


Elixir code cells also support autocompletion by pressing ctrl + ␣. The runtime must have started for autocompletion to work. A simple way to do so is by executing any code, such as the cell below:

"Hello world"

Now try autocompleting the code below to System.version(). First put the cursor after the . below and press ctrl + ␣:


You should have seen the editor listing many different options, which you can use to find version. Executing the code will return the Elixir version.

Note you can also press tab to cycle across the completion alternatives.

Getting the current directory

You can access the location of the current .livemd file and its current directory using __ENV__ and __DIR__ variables:


Mix projects

Sometimes you may want to run a notebook within the context of an existing Mix project. This is possible from Elixir v1.14 with the help of Mix.install/2.

As an example, imagine you have created a notebook inside your current project, at notebooks/example.livemd. In order to run within the root Mix project, using the same configuration and dependencies versions, you can change your notebook setup cell to invoke Mix.install/2 with the following arguments:

my_app_root = Path.join(__DIR__, "..")

    {:my_app, path: my_app_root, env: :dev}
  config_path: Path.join(my_app_root, "config/config.exs"),
  lockfile: Path.join(my_app_root, "mix.lock")


Livebook has a concept of runtime, which in practice is an Elixir node responsible for evaluating your code. You can choose the runtime by clicking the “Runtime” icon () on the sidebar (or by using the s r keyboard shortcut).

By default, a new Elixir node is started (similarly to starting iex). You can click reconnect whenever you want to discard the current node and start a new one.

You can also manually attach to an existing distributed node by picking the “Attached Node” runtime. To do so, you will need the Erlang Name of the external node and its Erlang Cookie. For example, you can start a Phoenix application as follows:

$ iex --sname phoenix-app --cookie secret -S mix phx.server

Now open up a new notebook and click the “Runtime” icon on the sidebar. Click to “Configure” the runtime and choose “Attached node”. Input the name and cookie as above and you should be ready to connect to it.

Note, however, that you can’t install new dependencies on a connected runtime. If you want to install dependencies, you have two options:

  1. Use the Mix project approach outlined in the previous section;

  2. Use a regular notebook and use Node.connect/1]( to connect to your application. Use the :erpc module to fetch data from the remote node and execute code.

More on branches #1

We already mentioned branching sections in Welcome to Livebook, but in Elixir terms each branching section:

  • runs in a separate process from the main flow
  • copies relevant bindings, imports and aliases from the parent
  • updates its process dictionary to mirror the parent

Let’s see this in practice:

parent = self()
Process.put(:info, "deal carefully with process dictionaries")

More on branches #2


Since this branch is a separate process, a crash has limited scope:

Process.exit(self(), :kill)

Evaluation vs compilation

Livebook automatically shows the execution time of each Code cell on the bottom-right of the cell. After evaluation, the total time can be seen by hovering the green dot.

However, it is important to remember that all code outside of a module in Elixir is evaluated, and therefore executes much slower than code defined inside modules, which are compiled.

Let’s see an example. Run the cell below:

Enum.reduce(1..1_000_000, 0, fn x, acc -> x + acc end)

We are adding all of the elements in a range by iterating them one by one. However, executing it likely takes some reasonable amount of time, as the invocation of the Enum.reduce/3 as well as the anonymous function argument are evaluated.

However, what if we move the above to inside a function? Let’s do that:

defmodule Bench do
  def sum do
    Enum.reduce(1..1_000_000, 0, fn x, acc -> x + acc end)

Now let’s try running it:


The latest cell should execute orders of magnitude faster than the previous Enum.reduce/3 call. While the call Bench.sum() itself is evaluated, the one million iterations of Enum.reduce/3 happen inside a module, which is compiled.

If a notebook is performing slower than expected, consider moving the bulk of the execution to inside modules.

Running tests

It is also possible to run tests directly from your notebooks. The key is to disable ExUnit‘s autorun feature and then explicitly run the test suite after all test cases have been defined:

ExUnit.start(autorun: false)

defmodule MyTest do
  use ExUnit.Case, async: true

  test "it works" do
    assert true

This helps you follow best practices and ensure the code you write behaves as expected!