Powered by AppSignal & Oban Pro

Jidoka: Workflow Patterns

livebook/16_workflow_patterns.livemd

Jidoka: Workflow Patterns

Run in Livebook

Workflows are deterministic, app-owned execution graphs. Use them when order, data flow, and failure behavior matter more than open-ended model reasoning.

Setup

Mix.install(
  [
    {:jidoka, git: "https://github.com/mikehostetler/jidoka.git", ref: "924a486f3c1b7e7a943cb3d5ceee0de65f158467"},
    {:kino, "~> 0.19.0"}
  ],
  config: [
    jidoka: [
      model_aliases: %{fast: "anthropic:claude-haiku-4-5"}
    ]
  ]
)
Jidoka.Kino.setup()

Define Workflow Steps

defmodule LivebookDemo.WorkflowPatterns.Fns do
  def classify(%{topic: topic, priority: priority, suffix: suffix}, _context) do
    {:ok, %{label: "#{topic}:#{priority}:#{suffix}", priority: priority}}
  end
end

defmodule LivebookDemo.WorkflowPatterns.Tools.RouteTicket do
  use Jidoka.Tool,
    name: "workflow_route_ticket",
    description: "Routes a classified ticket.",
    schema: Zoi.object(%{label: Zoi.string(), priority: Zoi.string()})

  @impl true
  def run(%{label: label, priority: "urgent"}, _context), do: {:ok, %{queue: "escalations", label: label}}
  def run(%{label: label}, _context), do: {:ok, %{queue: "standard", label: label}}
end

defmodule LivebookDemo.WorkflowPatterns.Tools.FailStep do
  use Jidoka.Tool,
    name: "workflow_fail_step",
    description: "Fails with a caller-provided reason.",
    schema: Zoi.object(%{reason: Zoi.string()})

  @impl true
  def run(%{reason: reason}, _context), do: {:error, reason}
end

Build A Context-Aware Workflow

defmodule LivebookDemo.WorkflowPatterns.RouteWorkflow do
  use Jidoka.Workflow

  workflow do
    id :livebook_route_workflow
    description "Classifies and routes a support topic."

    input Zoi.object(%{
            topic: Zoi.string(),
            priority: Zoi.string() |> Zoi.default("normal")
          })
  end

  steps do
    function :classify, {LivebookDemo.WorkflowPatterns.Fns, :classify, 2},
      input: %{
        topic: input(:topic),
        priority: input(:priority),
        suffix: context(:suffix)
      }

    tool :route, LivebookDemo.WorkflowPatterns.Tools.RouteTicket,
      input: from(:classify)
  end

  output from(:route)
end
{:ok, workflow} = Jidoka.inspect_workflow(LivebookDemo.WorkflowPatterns.RouteWorkflow)

Map.take(workflow, [:id, :description, :steps, :dependencies, :output])

Run with debug output to see the execution flow.

{:ok, debug} =
  Jidoka.Workflow.run(
    LivebookDemo.WorkflowPatterns.RouteWorkflow,
    %{topic: "billing", priority: "urgent"},
    context: %{suffix: "vip"},
    return: :debug
  )

rows =
  workflow.steps
  |> Enum.with_index(1)
  |> Enum.map(fn {step, order} ->
    %{
      order: order,
      step: step.name,
      waits_for: if(step.dependencies == [], do: "-", else: Enum.join(step.dependencies, ", ")),
      output: inspect(Map.fetch!(debug.steps, step.name))
    }
  end)

Jidoka.Kino.table("Workflow execution flow", rows)

Surface Validation And Failure

{:error, missing_context} =
  Jidoka.Workflow.run(
    LivebookDemo.WorkflowPatterns.RouteWorkflow,
    %{topic: "billing"},
    context: %{}
  )

Jidoka.format_error(missing_context)
defmodule LivebookDemo.WorkflowPatterns.FailingWorkflow do
  use Jidoka.Workflow

  workflow do
    id :livebook_failing_workflow
    input Zoi.object(%{reason: Zoi.string()})
  end

  steps do
    tool :fail, LivebookDemo.WorkflowPatterns.Tools.FailStep,
      input: %{reason: input(:reason)}
  end

  output from(:fail)
end

previous_level = Logger.level()
Logger.configure(level: :emergency)

{:error, failed} =
  try do
    Jidoka.Workflow.run(LivebookDemo.WorkflowPatterns.FailingWorkflow, %{reason: "boom"})
  after
    Logger.configure(level: previous_level)
  end

Jidoka.format_error(failed)