Oban Chore - A LiveView dashboard and plugin for managing Oban tasks

Oban Chore provides an Oban plugin to generate a dashboard to run Oban jobs

Motivation

In previous jobs, I often had to run Oban jobs manually for things like backfills or specific support actions. Doing this meant SSH-ing into production and manually typing commands to run the job, which carries inherent security risks.

Additionally, direct access to the Elixir node isn’t always available. While we could use the Oban dashboard, we often don’t want to give full access to this dashboard to non-technical staff. This unfortunately turns developers into a bottleneck for customer support and operations teams.

Oban Chore solves this by giving you a visual interface to handle these tasks directly from the browser, empowering non-technical users to safely run jobs.

Highlights

Zero-Boilerplate UI Generation: Define input schemas directly inside your worker, and let ObanChore generate the form components automatically.
Live Execution Streaming: Leveraging Phoenix PubSub and Telemetry, the dashboard streams logs and status updates from the process directly to the user’s browser in real-time.
Idempotency Check: Automatically detects if a job with the same arguments is already running.
Unique Execution Toggle: Manually enforce single-job execution via the dashboard UI. (you can override this from the job definition)
Validation: Full Ecto-backed validation for all chore arguments.

:rocket: Quick Example

  1. Configure Oban
    # config/config.exs                                                                                                                  
    config :my_app, Oban,                                                                                                                
      repo: MyApp.Repo,                                                                                                                  
      plugins: [                                                                                                                         
        {ObanChore.Plugin, otp_app: :my_app, pubsub_server: MyApp.PubSub}                                                                
      ],                                                                                                                                 
      queues: [default: 10]
  1. Define your Chore Worker:
defmodule MyApp.Chores.UserBackfill do                                                                                               
  use ObanChore.Worker,                                                                                                              
    name: "User Data Backfill",                                                                                                      
    description: "Backfills historical data for a specific user.",                                                                   
    fields: [                                                                                                                        
      user_id: [type: :integer, required: true, label: "Target User ID"],                                                            
      reason: [type: :string, default: "Manual correction", label: "Reason"]                                                         
    ]                                                                                                                                
                                                                                                                                     
  @impl Oban.Worker                                                                                                                  
  def perform(%Oban.Job{args: %{"user_id" => user_id, "reason" => reason}} = job) do                                                       
    ObanChore.log(job, "Starting user backfill for user #{user_id}")                                                                                 
    # Your execution logic here...                                                                                                   
    ObanChore.log(job, "Done!")                                                                                                      
    :ok                                                                                                                              
  end                                                                                                                                
                                                                                                                                     
  # Optional: Customize changeset validation                                                                                         
  @impl ObanChore.Worker                                                                                                             
  def custom_changeset(changeset) do                                                                                                 
    Ecto.Changeset.validate_number(changeset, :user_id, greater_than: 0)                                                             
  end                                                                                                                                
end                                                                                                                                  
  1. Mount the Dashboard:
# lib/my_app_web/router.ex                                                                                                           
defmodule MyAppWeb.Router do                                                                                                         
  use MyAppWeb, :router                                                                                                              
  import ObanChore.Router                                                                                                            
                                                                                                                                     
  scope "/" do                                                                                                                       
    pipe_through :browser                                                                                                            
    oban_chore_dashboard "/ops/chores"                                                                                               
  end                                                                                                                                
end                                                                                                                                  

Status & Feedback

Please note that this library is currently under active development. As this is an early release, any feedback, architecture suggestions, or ideas for new use cases are welcome!

Links

• Hex Package: oban_chore | Hex
• Documentation: ObanChore 🎭 — ObanChore v0.3.3

11 Likes

@alejolcc, nice! Thanks for sharing.

1 Like

hey, nice work. i’m trying to understand the intended boundary here.

if I am not mistaken Oban Web has authorization/access control via Oban.Web.Resolver.

from the README/thread, it looks like Oban Chore is more about exposing a curated set of predefined workers as validated forms for support/ops users: backfills, reruns, manual syncs, etc. that makes sense as a different use case from Oban Web, which already has authorization but is still primarily a full job/queue inspection and management dashboard.

a few things i’m curious about:

1. how do you see authorization being handled per chore/worker?
2. is there an audit trail for who ran what, with which args, and why?
3. do you expect chores to be idempotent by convention, or does the library enforce enough around duplicate execution?
4. would you recommend using this alongside Oban Web, with Oban Web reserved for developers/admins and Oban Chore for support/ops?

3 Likes

Hey! Thanks for the questions. As I mentioned before, this is still under heavy development. The core intention behind it is to provide a safe, user-friendly way for non-technical users to manually trigger background jobs.

Right now, to keep things simple, the dashboard relies on the host application’s router pipeline for access control (such as a generic live_session block or a basic auth plug). However, I want to introduce more granular access at the chore level. The plan is to add an optional, declarative auth/2 or authorized?/1 callback inside the ObanChore.Worker behaviour. This will allow developers to evaluate the current user session against a specific chore module before it even renders in the UI or permits execution.

This is on the backlog, but I don’t have a finalized plan for the underlying architecture just yet.

To make it a true, resilient audit trail, the data needs to be persistent, so an in-memory solution like ETS is out. My current thinking is to introduce a dedicated audit log table within the same Ecto repo that Oban uses.

But to keep installation friction as low as possible, I want this feature to be completely optional. If a developer wants to enable audit tracking, they would run a generator command—like a mix task or a native Igniter installer—to automatically inject all the required changes.

I’m very open to hearing how others have solved this type of optional, cross-cutting persistence in their own backend tools!

By convention, background jobs should always be idempotent, but that ultimately remains the developer’s responsibility. At the library level, ObanChore focuses on providing runtime guardrails in the UI to catch human error before it hits your business logic.

The dashboard enforces uniqueness by default by dynamically appending Oban’s native unique options (fields: [:args], states: [:available, :scheduled, :executing]) over a safe window. If a human double-clicks “Execute”, the dashboard blocks the duplicate insertion, and returns a clean warning banner in the UI. However, if a developer explicitly configures custom unique options directly inside their worker module, the default respects their specific settings. I also provide an “Allow concurrent executions” checkbox in the UI to serve as a conscious escape hatch if an operator intentionally needs to force a duplicate run.

Absolutely, this is NOT an Oban Web replacement. This is intended to safely run manual jobs defined by developers who know the logic.

Oban Web is an excellent engineering tool, but (I think) It’s built for low-level infrastructure inspection, monitoring system health, pausing queues, and debugging failed jobs. It belongs in the hands of developers and DevOps engineers who understand the technical internals of the system.

Oban Chore acts as a high-level business interface. It abstracts the raw job system entirely and exposes safe, user-friendly, and schema-validated forms. It belongs in the hands of support teams, account managers, and operations users who just need to trigger business processes (like running a report, onboarding a special client, or syncing an account) without ever seeing a stack trace or raw JSON payloads.

I would consider handling this at the router level, by allowing intentional collection of Chores into a route, ex:

oban_chore_dashboard "/ops", chores: [MyApp.Chores.FrontendDeploy]
oban_chore_dashboard "/customer_success", chores: [MyApp.Chores.UserBackfill]

Or perhaps via a tag system:

use ObanChore.Worker,                                                                                                              
    name: "User Data Backfill",                                                                                                      
    tag: :backfill

oban_chore_dashboard "/product", tags: [:backfill]

Developers could handle granular access via composition. This would allow ObanChore to stay light, neutral about auth, and most powerfully, work with any existing plug/pipe auth mechanism. The callbacks might be useful for advanced scenarios, but I’d start simpler!


I would also avoid building your own solution here, and consider what the lightest possible thing could be done to enable it. Optional lifecycle hooks in the worker module could get you very far, only invoked when triggered from ObanChore’s dashboards, which would allow people to plug in their own DB or telemetry based solutions. Something as simple as

use ObanChore.Worker

@impl ObanChore.Worker                                                                                                             
def on_chore_trigger(changeset)

would get you all you need to support auditing. Give it access to the conn to enable plug-drived auth, and with extra return value semantics, it could be the starting point for per-job auth as well, ex

def on_chore_trigger(conn, changeset) do
  if MyApp.Auth.Roles.ops?(conn.assigns.current_user) do
    {:ok, conn, changeset}
  else
    {:error, "Not authorized"}
  end
end
2 Likes

Interesting project. A few clarifications on what’s possible with Oban Web.

  1. As of v2.12 there’s a “New Job” sidebar where you can insert new jobs.
  2. There are roll based access to restrict CRUD operations to certain users, as well as fine grained access controls to restrict access to sensitive operations.
  3. Auditing is possible too, for all operations, by hooking into telemetry, or use the built-in audit logging.

Of course that doesn’t change whether it’s easy to identify some curated workers with default args and all that, but it’s possible to accomplish this with Oban Web.

2 Likes

Dude that’s awesome. Presumably if you define a new/2 function on the worker the you can do validations and stuff on the args?

4 Likes

maybe would be a good idea to select existing workers and pass arguments

1 Like

Currently released versions, no.
On main and because you mentioned it..is now true and very much the case. :magic_wand:

4 Likes

Thanks!! This is fantastic feedback. I really like both of these suggestions, they align perfectly with keeping the library lightweight which is my intention :slightly_smiling_face:

1 Like

Thanks Soren! Oban Web is fantastic and I love it.

My main goal with this lib is to focus strictly on the developer experience of turning workers into schema-validated HTML forms (instead of raw JSON fields). I want to provide a quick, out-of-the-box solution with a friendlier UI for non-technical users, making it easier for developers to hand off specific manual tasks safely.

2 Likes