# 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.