Jidoka: Workflows And Imports
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)