Powered by AppSignal & Oban Pro

Jidoka: Workflows And Imports

livebook/03_workflows_and_imports.livemd

Jidoka: Workflows And Imports

Run in Livebook

Use deterministic workflows for multi-step application logic, expose a workflow to an agent, and import an agent from JSON.

Setup

Chat cells use a Livebook secret named ANTHROPIC_API_KEY. The direct workflow and import cells run without a provider key.

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 Tools

defmodule LivebookDemo.Tools.AddNumbers do
  use Jidoka.Tool,
    name: "add_numbers",
    description: "Adds two integers.",
    schema:
      Zoi.object(%{
        a: Zoi.integer(),
        b: Zoi.integer()
      }),
    output_schema:
      Zoi.object(%{
        sum: Zoi.integer()
      })

  @impl true
  def run(%{a: a, b: b}, _context) do
    {:ok, %{sum: a + b}}
  end
end

defmodule LivebookDemo.Tools.DoubleNumber do
  use Jidoka.Tool,
    name: "double_number",
    description: "Doubles an integer.",
    schema:
      Zoi.object(%{
        value: Zoi.integer()
      }),
    output_schema:
      Zoi.object(%{
        value: Zoi.integer()
      })

  @impl true
  def run(%{value: value}, _context) do
    {:ok, %{value: value * 2}}
  end
end

Build A Workflow

Use workflows when application code should own an ordered process.

defmodule LivebookDemo.Workflows.MathPipeline do
  use Jidoka.Workflow

  workflow do
    id :livebook_math_pipeline
    description "Adds one to a value and doubles the result."

    input Zoi.object(%{
            value: Zoi.integer()
          })
  end

  steps do
    tool :add, LivebookDemo.Tools.AddNumbers,
      input: %{
        a: input(:value),
        b: value(1)
      }

    tool :double, LivebookDemo.Tools.DoubleNumber,
      input: %{
        value: from(:add, :sum)
      }
  end

  output from(:double)
end

Inspect and run the workflow. The key detail is the execution order: :double depends on :add, so Jidoka runs add_numbers first, passes its sum into double_number, and returns the final output from :double.

{:ok, workflow} = Jidoka.inspect_workflow(LivebookDemo.Workflows.MathPipeline)
{:ok, debug} = Jidoka.Workflow.run(LivebookDemo.Workflows.MathPipeline, %{value: 20}, return: :debug)

%{
  workflow: Map.take(workflow, [:id, :description, :steps, :dependencies, :output]),
  run_output: debug.output,
  run_steps: debug.steps
}

Show the same run as an ordered execution table:

flow_rows =
  workflow.steps
  |> Enum.with_index(1)
  |> Enum.map(fn {step, order} ->
    produced = Map.fetch!(debug.steps, step.name)

    %{
      order: order,
      step: step.name,
      waits_for: if(step.dependencies == [], do: "-", else: Enum.join(step.dependencies, ", ")),
      runs: step.target |> inspect() |> String.replace_prefix("Elixir.", ""),
      produced: inspect(produced)
    }
  end)

Jidoka.Kino.table("Workflow execution order", flow_rows)

Trigger a validation error and format it for a user:

{:error, reason} = Jidoka.Workflow.run(LivebookDemo.Workflows.MathPipeline, %{value: "twenty"})
Jidoka.format_error(reason)

Expose The Workflow To An Agent

Workflow capabilities publish a workflow as a model-visible tool. The agent sees one callable tool named run_math_pipeline; the model does not manually call :add and :double. When the tool is called, Jidoka runs the workflow runtime, which executes those ordered steps from the table above.

defmodule LivebookDemo.WorkflowAgent do
  use Jidoka.Agent

  agent do
    id :livebook_workflow_agent
  end

  defaults do
    model :fast
    instructions "Use workflow tools for arithmetic processes. Return the final result clearly."
  end

  capabilities do
    workflow LivebookDemo.Workflows.MathPipeline,
      as: :run_math_pipeline,
      description: "Add one to an integer and double the result.",
      result: :structured
  end
end
{:ok, definition} = Jidoka.inspect_agent(LivebookDemo.WorkflowAgent)

Map.take(definition, [:id, :tool_names, :workflow_names, :workflows])

Call the generated workflow tool directly. This is the same tool surface the agent exposes to the model.

workflow_tool =
  Enum.find(LivebookDemo.WorkflowAgent.tools(), fn tool ->
    Code.ensure_loaded?(tool) and function_exported?(tool, :name, 0) and
      tool.name() == "run_math_pipeline"
  end)

{:ok, tool_call} = workflow_tool.run(%{value: 20}, %{})

%{
  model_visible_tool: workflow_tool.name(),
  generated_tool_module: workflow_tool,
  tool_schema: workflow_tool.schema(),
  tool_call_output: tool_call,
  ordered_steps:
    Enum.map(workflow.steps, fn step ->
      %{
        step: step.name,
        waits_for: step.dependencies,
        runs: step.target
      }
    end)
}

Now ask the agent to use that workflow tool. The model chooses the single run_math_pipeline capability; the workflow runtime still owns the :add then :double execution flow.

{:ok, pid} =
  Jidoka.Kino.start_or_reuse("livebook-workflow-agent", fn ->
    LivebookDemo.WorkflowAgent.start_link(id: "livebook-workflow-agent")
  end)

Jidoka.Kino.chat("Workflow capability call", fn ->
  LivebookDemo.WorkflowAgent.chat(pid, "Use the run_math_pipeline workflow with value 20. What is the output?")
end)

Import A JSON Agent

Imported agents are constrained runtime specs. Executable capabilities resolve through explicit registries supplied by the application.

json =
  Jason.encode!(%{
    agent: %{
      id: "livebook_imported_math",
      description: "Imported math assistant",
      context: %{
        "tenant" => "demo",
        "channel" => "livebook"
      }
    },
    defaults: %{
      model: "fast",
      instructions: "Use available tools when they help. Keep answers short."
    },
    capabilities: %{
      tools: ["add_numbers"]
    }
  })

{:ok, imported_agent} =
  Jidoka.import_agent(json,
    available_tools: [LivebookDemo.Tools.AddNumbers]
  )

{:ok, imported_definition} = Jidoka.inspect_agent(imported_agent)

Map.take(imported_definition, [:id, :description, :context, :tool_names])

Encode the imported agent back to YAML:

case Jidoka.encode_agent(imported_agent, format: :yaml) do
  {:ok, yaml} -> yaml
  {:error, reason} -> Jidoka.format_error(reason)
end
{:ok, pid} =
  Jidoka.Kino.start_or_reuse("livebook-imported-agent", fn ->
    Jidoka.start_agent(imported_agent, id: "livebook-imported-agent")
  end)

Jidoka.Kino.chat("Imported-agent tool call", fn ->
  Jidoka.chat(
    pid,
    "Use the add_numbers tool to add 7 and 35. Reply with only the sum.",
    context: %{"session" => "livebook-imported"}
  )
end)