Workflow Composition Agent

Copy Markdown View Source

This Livebook proves the complete workflow and graph composition section with one deterministic fulfillment agent. It does not use a provider key or a network request.

package_root = Path.expand("../..", __DIR__)

Mix.install(
  [
    {:kino, "~> 0.14"},
    {:jidoka, path: package_root}
  ],
  consolidate_protocols: false,
  lockfile: Path.join(package_root, "mix.lock")
)

Code.require_file(Path.expand("../loader.exs", __DIR__))
JidokaExamples.Loader.load!(__DIR__)

alias Jidoka.Workflow.{Background, Scheduler}
alias JidokaExamples.WorkflowComposition.Agent, as: FulfillmentAgent
alias JidokaExamples.WorkflowComposition.{FulfillmentWorkflow, Scenario}

Jidoka.Kino.setup_notebook(model: "test:workflow-composition-script", check_provider?: false)

Inspect The Agent And Graph

The agent has one operation named fulfill_order. That operation owns the whole workflow graph.

agent_view = Jidoka.inspect(FulfillmentAgent)
{:ok, workflow_spec} = Jidoka.Workflow.definition(FulfillmentWorkflow)

%{
  agent_operations: Enum.map(agent_view.spec.operations, & &1.name),
  workflow_id: workflow_spec.id,
  step_kinds: Enum.map(workflow_spec.steps, &{&1.name, &1.kind})
}

Run Two Static Agent Nodes

This graph has one draft agent and one review agent. The dependency edge and termination are static. It does not claim a dynamic team or group-chat runtime.

{:ok, _agent_node_result} = Scenario.static_multi_agent()

Run Directly And Through The Agent

This cell proves that direct execution and the agent tool return the same result. The result also shows the selected branch, two inventory attempts, bounded loop output, and one dynamically created item.

{:ok, direct_report} = Scenario.direct_and_agent(observer: self())

unless direct_report.parity? and
         direct_report.direct_output.reservation_attempts == 2 and
         direct_report.direct_output.shipped == ["starter_kit", "cable", "welcome_card"] do
  raise "direct and agent workflow results did not match"
end

%{
  agent_answer: direct_report.agent_answer,
  direct_output: direct_report.direct_output,
  parity?: direct_report.parity?
}

Start The Runtime Services

The setup is repeatable. Re-evaluating this cell reuses the named processes when they are already running.

runner = JidokaExamples.WorkflowComposition.LivebookRunner
scheduler = JidokaExamples.WorkflowComposition.LivebookScheduler
now = ~U[2026-08-01 12:00:00Z]

start_once = fn name, start_fun ->
  case Process.whereis(name) do
    nil -> start_fun.()
    pid -> {:ok, pid}
  end
end

{:ok, _runner_pid} = start_once.(runner, fn -> Background.start_link(name: runner) end)

{:ok, _scheduler_pid} =
  start_once.(scheduler, fn ->
    Scheduler.start_link(
      name: scheduler,
      runner: runner,
      auto_schedule: false,
      clock: fn -> now end
    )
  end)

%{runner: runner, scheduler: scheduler}

Run In The Background

The caller receives a stable ID. It can reconnect to the run and read safe lifecycle events without the original caller process.

{:ok, background_report} =
  Scenario.background(runner, run_id: Jidoka.Id.generate!("workflow_livebook"))

unless background_report.run.status == :completed and background_report.events != [] do
  raise "the background workflow did not complete with event evidence"
end

%{
  run: background_report.run,
  event_types: Enum.map(background_report.events, & &1.type)
}

Run From A Schedule

The schedule does not create a separate execution type. It submits the same workflow to the background runner and records the trigger result.

{:ok, scheduled_report} =
  Scenario.scheduled(scheduler, runner, now,
    schedule_id: Jidoka.Id.generate!("schedule_livebook")
  )

unless scheduled_report.trigger.status == :started and
         scheduled_report.run.status == :completed do
  raise "the scheduled workflow did not complete"
end

%{
  schedule: scheduled_report.schedule,
  trigger: scheduled_report.trigger,
  run: scheduled_report.run
}