Powered by AppSignal & Oban Pro

Getting Started

guides/start-here/getting-started.livemd

Getting Started

This tutorial installs Jido and takes you from one Action to a running Agent that keeps a payment total. Each later tutorial in this section slows down on one of these pieces.

You need Elixir 1.18 or later. Everything runs locally, with no external service or API key.

Install Jido

Run this cell in Livebook.

Mix.install([
  {:jido, github: "mikehostetler/new_jido", branch: "main"}
])

In a Mix project, add the same tuple to deps/0 in mix.exs, then run mix deps.get. Jido starts its own OTP application, so it needs no other setup before the next step.

Write an Action

An Action is one named unit of work. This one adds a payment to an invoice balance. Its schema rejects a payment that is not a positive integer. It reads the current balance from the :agent_state key of its context and returns the complete next state.

defmodule GettingStarted.ApplyPayment do
  use Jido.Action,
    name: "getting_started_apply_payment",
    schema: Jido.Joi.object(%{amount_cents: Jido.Joi.integer() |> Jido.Joi.min(1)})

  @impl true
  def run(%{amount_cents: amount}, %{agent_state: state}) do
    {:ok, %{state | paid_cents: state.paid_cents + amount}}
  end
end

Run the Action

Jido.Exec.run/4 takes the Action, its input, and a context map. It validates the input, runs the Action, and returns the result. An Agent fills in :agent_state later. For now, pass it by hand.

context = %{agent_state: %{paid_cents: 500}}

Jido.Exec.run(GettingStarted.ApplyPayment, %{amount_cents: 1_500}, context)

The cell returns {:ok, %{paid_cents: 2000}}.

Invalid input never reaches run/2. You get an error value that you can match on.

{:error, %Jido.Error.ValidationError{} = error} =
  Jido.Exec.run(GettingStarted.ApplyPayment, %{amount_cents: 0}, context)

Exception.message(error)

The cell returns "too small: must be at least 1, at amount_cents".

Define an Agent

An Agent adds state that lasts across Signals. A route connects a Signal type to the Action. Jido supplies the current state in :agent_state, and the Action's output becomes the next state.

defmodule GettingStarted.InvoiceAgent do
  use Jido.Agent, name: "getting_started_invoice"

  state do
    schema Jido.Joi.object(%{
             total_cents: Jido.Joi.integer(),
             paid_cents: Jido.Joi.integer() |> Jido.Joi.default(0)
           })
  end

  routes do
    signal_source "/billing/invoice"

    route "invoice.payment.apply", GettingStarted.ApplyPayment, as: :apply_payment
  end
end

as: :apply_payment generates apply_payment_signal!/2, which builds the Signal for this route.

Run one Turn without a process

An instance is the definition plus an ID and state. It is a plain value. cmd/3 runs one Turn and returns the next value with a list of Directives.

invoice = GettingStarted.InvoiceAgent.instantiate!(id: "inv-1001", state: %{total_cents: 5_000})
payment = GettingStarted.InvoiceAgent.apply_payment_signal!(%{amount_cents: 1_500})

{:ok, paid, []} = GettingStarted.InvoiceAgent.cmd(invoice, payment)

{invoice.state.paid_cents, paid.state.paid_cents}

The cell returns {0, 1500}. The source value did not change, and no process started.

Run the Agent live

To keep the Agent running, start a Jido instance and start the Agent inside it. Jido.AgentServer.call/3 sends a Signal and waits for the commit.

{:ok, _instance} = Jido.start(name: GettingStarted.Jido)

{:ok, server} =
  Jido.start_agent(GettingStarted.Jido, GettingStarted.InvoiceAgent,
    id: "inv-1001",
    state: %{total_cents: 5_000}
  )

:ok = Jido.AgentServer.await_ready(server)

{:ok, _agent, []} = Jido.AgentServer.call(server, payment)
{:ok, _agent, []} = Jido.AgentServer.call(server, payment)

Jido.AgentServer.agent(server).state

The cell returns %{paid_cents: 3000, total_cents: 5000}. The server kept the state between the two calls.

A failed Turn leaves the committed state alone, and the server keeps running.

bad_payment = GettingStarted.InvoiceAgent.apply_payment_signal!(%{amount_cents: -1})

{:error, %Jido.Error.ValidationError{}} = Jido.AgentServer.call(server, bad_payment)

Jido.AgentServer.agent(server).state.paid_cents

The cell returns 3000.

Stop the instance

Jido.stop/1 stops the instance and every Agent in it.

:ok = Jido.stop(GettingStarted.Jido)

Process.alive?(server)

The cell returns false.

Next steps