Skip to main content

guides/getting_started.md

# Getting Started

Baton turns a set of Oban jobs into a dependency-ordered workflow: you declare
which steps depend on which, and Baton guarantees execution order, passes
results between steps, and gates each step at runtime — all on Oban OSS, no Oban
Pro required.

This guide gets you from zero to a running workflow. For a fuller example with
parallelism and a live UI, see the
[building a workflow guide](building_a_workflow.md).

## 1. Install

Add Baton alongside Oban:

```elixir
def deps do
  [
    {:baton, "~> 0.1"},
    {:oban, "~> 2.17"}
  ]
end
```

Add Baton's tables in a migration. `up/0` installs the latest schema and is
idempotent, so the same migration can be re-run to pick up future versions:

```elixir
defmodule MyApp.Repo.Migrations.AddBaton do
  use Ecto.Migration
  def up, do: Baton.Migration.up()
  def down, do: Baton.Migration.down()
end
```

## 2. Configure

Baton inherits its repo from Oban, so you don't pass one. Register
`Baton.Plugin` in your Oban `plugins:` list and give Oban a queue to run on:

```elixir
# config/config.exs
config :my_app, Oban,
  repo: MyApp.Repo,
  queues: [default: 20],
  plugins: [
    Oban.Plugins.Pruner,
    {Baton.Plugin, interval: :timer.seconds(60)}
  ]
```

Baton-specific settings are optional — these are the defaults:

```elixir
config :baton,
  oban_name: Oban,              # the Oban instance to use
  pubsub: nil,                  # set to MyApp.PubSub for live events (step 5)
  pricing: Baton.Pricing.Default # only used if you track LLM cost
```

The plugin keeps workflows healthy on each sweep — it rescues jobs orphaned by
pruning, emits failure telemetry, backstops the terminal "workflow finished"
notification, and (opt-in) prunes Baton's own tables. See `Baton.Plugin`.

## 3. Define steps

A step is a module that does `use Baton.Worker` and implements
`perform_workflow/1`. It's a drop-in for `Oban.Worker` — the same options
(`queue:`, `max_attempts:`, etc.) apply — but you implement `perform_workflow/1`
instead of `perform/1`, and Baton handles dependency gating, result storage, and
broadcasting around it.

Return one of:

| Return | Meaning |
|--------|---------|
| `:ok` | success, no result to pass downstream |
| `{:ok, result}` | success; `result` is stored and passed to dependents |
| `{:error, reason}` | failure; retried per `max_attempts`, then discarded |
| `{:snooze, seconds}` | not ready; recheck later without consuming an attempt |
| `{:discard, reason}` | give up now, without retrying |

```elixir
defmodule MyApp.Steps.Fetch do
  use Baton.Worker, queue: :default, max_attempts: 3

  @impl true
  def perform_workflow(%Oban.Job{args: %{"url" => url}}) do
    {:ok, %{"body" => HTTPClient.get!(url)}}
  end
end
```

Downstream steps read a dependency's stored result with
`Baton.Results.get_result/2`:

```elixir
defmodule MyApp.Steps.Parse do
  use Baton.Worker, queue: :default
  alias Baton.Results

  @impl true
  def perform_workflow(%Oban.Job{} = job) do
    {:ok, %{"body" => body}} = Results.get_result(job, :fetch)
    {:ok, %{"parsed" => MyApp.Parser.run(body)}}
  end
end
```

## 4. Build and insert

Chain `Baton.add/4`, declaring dependencies with `deps:`. The whole workflow is
validated (no cycles, every dep exists) and inserted in a single transaction —
it's all-or-nothing:

```elixir
Baton.new(workflow_name: "ingest")
|> Baton.add(:fetch, MyApp.Steps.Fetch.new(%{url: "https://example.com"}))
|> Baton.add(:parse, MyApp.Steps.Parse.new(%{}), deps: [:fetch])
|> Baton.add(:store, MyApp.Steps.Store.new(%{}), deps: [:parse])
|> Baton.insert!()
```

`insert!/1` returns the inserted jobs or raises on a bad graph; `insert/1`
returns `{:ok, jobs}` or `{:error, {message, reason}}`. To check a graph without
inserting, use `Baton.validate/1`.

Oban runs `fetch` immediately (no deps); `parse` and `store` gate themselves and
start as soon as their dependencies complete.

## 5. Observe

Everything observable is emitted as telemetry, so you can integrate without
Phoenix. Attach the built-in logger in dev to see failures and rescues:

```elixir
Baton.Telemetry.attach_default_logger()
```

Key events (see `Baton.Telemetry`):

- `[:baton, :step, <state>]` — every step transition
- `[:baton, :workflow, :finished]` — a workflow settled (`:completed`/`:failed`)
- `[:baton, :plugin, :rescued]` / `[:baton, :plugin, :pruned]` — plugin activity

If you set `config :baton, pubsub: MyApp.PubSub`, the same step transitions are
also broadcast over `Phoenix.PubSub` for a live dashboard — see the
[building a workflow guide](building_a_workflow.md) for a LiveView example.

## Next steps

- [Building a workflow](building_a_workflow.md) — fan-out/fan-in, pruning, and a
  live LiveView.
- [Multi-model workflows](multi_model.md) — run a step across several models and
  synthesize the results, and track per-step LLM cost.
- `Baton.LLMWorker` — LLM-tuned worker with jittered backoff, idempotent
  retries, and automatic cost/usage recording.