How to setup recurring jobs with Oban in Elixir

async oban bamboo

Oban has proven itself to be the most versatile job processing library in Elixir and Phoenix. Coming from Sidekiq, it feels very familiar and supports scenarios with async jobs, jobs to be performed in a certain time in the future, and recurring jobs based on a cron schedule.

The latter is especially great if you want to setup a daily digest email to yourself or your users.

In this tutorial will go through how to add Oban and create a daily digest email that I send out with Bamboo.

Step 1 - Download a premade boilerplate

As many of the tutorials, I need some prerequisits. In this case, I want a premade Phoenix app with Pow authenication and Bamboo email.

Generate a boilerplate

After the boilerplate is downloaded, I need to run

mix deps.get
mix ecto.setup
yarn install --cwd assets

Step 2 - Install Oban

Oban is a well documented library an the installation instructions are pretty straight forward. First add it to the mix file.

# mix.exs
def deps do
    {:oban, "~> 2.6.1"}

Oban is dependent on storing the job queues in in postgres database so after the reps are fetched, I ned to generate the migration file for it

mix deps.get
mix ecto.gen.migration add_oban_jobs_table

And paste in this code that comes directly from the installation instructions:

defmodule Tutorial.Repo.Migrations.AddObanJobsTable do
  use Ecto.Migration  def up do
  end  # We specify <code class="inline-code">version: 1</code> in <code class="inline-code">down</code>, ensuring that we'll roll all the way back down if
  # necessary, regardless of which version we've migrated <code class="inline-code">up</code> to.
  def down do
    Oban.Migrations.down(version: 1)

Run the migrations by

mix ecto.migrate

These are the basic configuration recommended in the get started guide. You could tweak the queue priorizations so it fits your needs.

Most likely, you don't want to start Oban to work off queues in tests so turn it off in the test config by adding:

# config/test.exs
config :tutorial, Oban, queues: false, plugins: false

With the configurations in place, add the Oban inside the clirdrens list so its started when the application starts.

# lib/tutorial/application.ex
defmodule Tutorial.Application do
  use Application  def start(<i>type, </i>args) do
    children = [
      {Phoenix.PubSub, name: Tutorial.PubSub},
      {Oban, oban_config()} # Add this line
    ]    opts = [strategy: :one<i>for</i>one, name: Tutorial.Supervisor]
    Supervisor.start_link(children, opts)
  end  defp oban_config do # Add this line
    Application.fetch_env!(:tutorial, Oban)

That should conclude the basic installation and setup of Oban in a Elixir application.

Step 3 - Add the daily digest mailer

Next step is to setup the Daily digest email. This will be a very simple daily digest. I just want to receive an email on all users that have signed up in the last 24 hours.

# lib/tutorial/users.ex
defmodule Tutorial.Users do
  @moduledoc """
  The Users context.
  """  import Ecto.Query, warn: false
  alias Tutorial.Repo
  alias Tutorial.Users.User  def list_users_from_last_day do
      u in User,
      where: u.inserted_at > ago(1, "day")
    |> Repo.all()

Since I want the email to be sent at the same time every day, I dont need to specify the time in the query.

Since I picked a boilerplate with Bamboo installed, I already have the mailer and some emails setup. But I need to add my digest_email that takes the users:

# lib/tutorial/emails.ex
defmodule Tutorial.Email do
  import Bamboo.Email  def digest_email(users) do
    |> subject("Daily Digest")
    |> to("myself@example.com")
    |> render("daily_digest.html", title: "Daily Digest Email", users: users)
  end  defp base_email do
    |> from(@from)
    |> put_html_layout({TutorialWeb.LayoutView, "email.html"}) # Set default layout
    |> put_text_layout({TutorialWeb.LayoutView, "email.text"}) # Set default text layout

I want to content of the email in a template file:

<!-- lib/tutorial_web/templates/email/daily_digest.html.eex -->
<%= if Enum.count(@users) == 0 do %>
  <h1>No users signed up since yesterday</h1>
<% else %>
  <h1>These users have joined since yesterday</h1>  <%= for user <- @users do %>
    <%= user.email %><br>
  <% end %>
<% end %>

I can now test this in IEX

Tutorial.Users.list_users_from_last_day() |> Tutorial.Emails.digest_email() |> Tutorial.Mailer.deliver_now()

And I should get get the variant that not user have signed up since I havent added any users.

However, If I start the app and register a user:

Tutorial.Users.list_users_from_last_day() |> Tutorial.Emails.digest_email() |> Tutorial.Mailer.deliver_now()

If I run the code again, then I can see that I have a new user registered since yesterday. And with that part working, I will need to move that code from IEX to a worker and have that triggered by Oban.

Step 4 - Configure Oban to run daily

First I want to store my workers in a separate folder. So I create a workers folder and add the daily digest worker in there. I will do the exact same thing as the command I ran in the IEX console.

# lib/tutorial/workers/daily_digest_worker.ex
defmodule Tutorial.Workers.DailyDigestWorker do
  use Oban.Worker  alias Tutorial.{Users, Emails, Mailer}  @impl Oban.Worker
  def perform(_job) do
    |> Emails.digest_email()
    |> Mailer.deliver_now()    :ok

The last piece of the puzzle is to configure Oban to run the worker daily at a specific time. Oban supports the normal cron syntax for this. In this case, I want to run this at 08:00 (server time) every day.

# config/config.exs
config :tutorial, Oban,
  repo: Tutorial.Repo,
  queues: [default: 10, mailers: 20, events: 50, low: 5],
  plugins: [
     crontab: [
       {"0 8 * * *", Tutorial.Workers.DailyDigestWorker},

Related Tutorials

Published 17 Feb - 2023

Run one off tasks in Phoenix

If you run a web application with users in production, you have surely had the need to run a task that changes the database for one or more users. T..