Host App Integration

Copy Markdown View Source

This document defines the initial integration contract for:

  • Phoenix applications
  • OTP applications with an existing Repo
  • existing installations that already run background jobs

Tested Toolchain

Current CI and onboarding smoke tests run with:

  • Erlang/OTP 28.4.1
  • Elixir 1.19.5-otp-28
  • Jido 2.0+

Installation

Add :jizoku to the host application's dependencies and fetch dependencies as usual with Mix.

Preferred Hex dependency:

defp deps do
  [
    {:jizoku, "~> 0.3.7"}
  ]
end

If the host app defines custom steps with use Jido.Action, add :jido explicitly to the host app as well rather than relying on a transitive dependency:

defp deps do
  [
    {:jido, "~> 2.0"},
    {:jizoku, "~> 0.3.7"}
  ]
end

Then install Jizoku's library-owned migrations into the host app:

mix jizoku.install
mix ecto.migrate
mix jizoku.doctor --json --fail-on-drift

mix jizoku.install creates one current-schema Jizoku migration in the host application's priv/repo/migrations directory. It does not install or run migrations for the host application's job backend. The doctor command performs a read-only structural check and gives CI a nonzero gate when the migrated host database is behind or incompatible with Jizoku's required baseline.

Configuration

Start with three pieces:

  1. Jizoku config points at the host repo and runtime boundary.
  2. The journal runtime owns its dispatch queue through Jizoku config; the host app only needs a worker process that calls Jizoku.execute_next/1.
  3. Journal workers call Jizoku.execute_next/1 to claim and execute visible attempts.

The host application configures Jizoku under the :jizoku application:

config :jizoku,
  repo: MyApp.Repo,
  partition: "tenant_acme",
  queue: "default"

Host config keys:

  • :repo - required for the default Ecto-backed journal setup; Jizoku uses it to infer {Jizoku.Runtime.Journal.Storage.Ecto, repo: MyApp.Repo} when journal_storage: is omitted

Optional keys:

  • :runtime - :journal by default; routes public start, execution, and manual-control APIs through the Jido-native journal runtime
  • :read_model - :read_model by default; routes inspection, graph inspection, and explanation through journal projections
  • :journal_storage - optional for the default Ecto-backed setup; when omitted, Jizoku uses {Jizoku.Runtime.Journal.Storage.Ecto, repo: MyApp.Repo}. Set it only to override the storage adapter. Explicit nil is rejected for journal-backed runtime or read-model paths.
  • :queue - "default" by default; selects the journal dispatch queue used by the configured journal runtime and read model
  • :partition - omitted by default, preserving the exact legacy journal namespace. A validated string scopes run, dispatch, workflow-index, global-catalog, and checkpoint identities together.

When a host uses partitions, it must route the same trusted :partition through start, worker, cron, signal, control, replay, and inspection calls. Run UUIDs and queue names may repeat across partitions. Jizoku does not search another partition when a lookup misses, and a partition is not an authorization boundary; authorize tenant or domain access in the host before selecting it.

Enabling a partition is a namespace cutover, not an in-place migration. Existing unpartitioned runs remain in the legacy namespace and do not appear in partitioned lists or recovery. Drain them there or perform an explicit, application-owned migration before changing worker routing. Rolling the config back selects the legacy namespace again; it does not merge partitioned history.

Public Jizoku.start/2, start/3, and start/4 calls resolve those defaults through the application environment too. If a host app starts runs manually from IEx, a Phoenix controller, or another direct boundary, it still needs config :jizoku, repo: MyApp.Repo when it wants the default inferred Ecto storage. Hosts that already own a storage adapter boundary can skip global repo: config and pass an explicit journal_storage: override instead.

Stale-worker handling comes from journal claim fencing or the host backend's lease system.

For most host apps, the inferred Ecto storage is the recommended starting point when MyApp.Repo uses Postgres or a Postgres-compatible Ecto adapter. It persists Jido threads and checkpoints in Jizoku's installed tables and keeps journal storage in the same transactional database boundary as the host app. The boundary remains adapter-shaped, so other Jido-compatible stores can be used later, but production stores must still provide ordered per-thread appends, durable checkpoint reads, and conflict detection for :expected_rev. See Storage strategy for the full adapter contract and compatibility expectations.

The current journal default covers start, cron start, cancellation, replay, global and workflow-filtered list_runs/2, inspect, explain, graph inspection, manual resume/approval controls, and Jizoku.execute_next/1. Journal listing is backed by a durable run catalog fact rather than a storage-adapter scan, and returns redacted summaries; use inspect_run/2 for one run when a caller needs inputs, outputs, attempts, or claim metadata. Dashboards can call list_runs([]) for the index view, then pass the selected summary's partition, run_id, and queue to inspect_run(run_id, partition: partition, queue: queue, include_history: true) or inspect_run_graph(run_id, partition: partition, queue: queue) for detail views.

Do not serialize inspection or graph detail directly to untrusted clients. Host apps should authorize the caller, select only the fields the view needs, and redact host-domain inputs, outputs, errors, manual metadata, idempotency keys, and claim identifiers before returning the payload. See Observability.

Runtime Boundaries

Most host apps can use Jizoku without writing Jido agents, storage calls, or Bedrock code. The public integration boundary is:

Jido is the runtime foundation behind that boundary. Jizoku uses Jido journals, storage callbacks, actions, and rebuildable agents internally so run state can be reconstructed from durable facts. Users only need to learn those details when they are contributing to the runtime, replacing the default journal storage adapter, or debugging low-level runtime behavior.

Bedrock is optional. Use the basic execute_next/1 worker loop when a host only needs Jizoku to claim visible journal work from the configured storage. Use Bedrock or another lease-capable backend when the host needs backend-owned delivery, delayed visibility, worker leases, heartbeats, retry requeue, dead-letter handling, or stale-worker recovery outside the Jizoku journal. Those backend concerns belong in adapter modules, not workflow modules.

Journal Worker Contract

Step execution is pulled by host-owned workers. A minimal worker can be a small GenServer loop under the host supervision tree:

defmodule MyApp.JizokuWorker do
  use GenServer

  def start_link(opts \\ []) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

  def init(opts) do
    {:ok, %{owner_id: Keyword.get(opts, :owner_id, "my-app-jizoku")}, {:continue, :drain}}
  end

  def handle_continue(:drain, state), do: {:noreply, drain_once(state)}
  def handle_info(:drain, state), do: {:noreply, drain_once(state)}

  defp drain_once(state) do
    interval =
      case Jizoku.execute_next(
             owner_id: state.owner_id,
             lease_for: 30,
             heartbeat_interval_ms: 10_000
           ) do
        {:ok, :none} -> 100
        {:ok, _snapshot} -> 0
        {:error, _reason} -> 1_000
      end

    Process.send_after(self(), :drain, interval)
    state
  end
end

This loop is intentionally small. Production hosts can add capacity limits, back-pressure, node placement, metrics, and shutdown policy around the same public call. Jizoku still owns the journaled claim, completion, retry, manual-control, and terminal-state facts.

lease_for and heartbeat_interval_ms are journal executor controls, not an external backend requirement. Hosts without Bedrock or another leased job backend may still pass them when steps can run longer than a claim window. Oban OSS workers fall into this plain-host category for this purpose: keep Oban job delivery concerns separate and let Jizoku.execute_next/1 maintain the journal claim lease. Short step workers can omit heartbeat_interval_ms. Hosts that also use a backend lease must maintain that backend lease separately from the journal claim lease. The runtime rejects intervals below 50ms to keep heartbeat write volume bounded.

Telemetry Integration

Jizoku emits public runtime events under [:jizoku, :runtime, ...] for command application, executor polls, step invocation, and committed lifecycle facts. No runtime config is required to enable emission. Hosts attach handlers or supervise their selected reporter/exporter and can use Jizoku.Telemetry.metrics/0 as the default bounded-cardinality metric set.

defmodule MyApp.Metrics do
  def metrics do
    application_metrics() ++ Jizoku.Telemetry.metrics()
  end
end

Use Jizoku.Telemetry.partition_metrics/0 only after accepting the tenant or domain cardinality of the configured partition namespace. Correlation fields such as run, signal, runnable, and trace IDs are suitable for traces or authorized diagnostic logs, but not metric labels.

Lifecycle point events follow successful journal appends. Jizoku-owned Ecto step transactions buffer completion events until commit and discard them on rollback; arbitrary host-owned outer transactions are outside that guarantee. The events remain best-effort and do not replace journal-backed inspection. See Observability for the full event, metadata, privacy, and delivery contract.

Multi-node Journal Workers

Multiple host application nodes may drain the same Jizoku queue. They do not form a Jizoku cluster and do not require Distributed Erlang. Each node runs the same small worker loop against shared durable journal storage, and the journal claim is the cross-node ownership boundary.

Use this deployment contract:

  • point every worker for a logical queue at the same production journal storage and the same queue value
  • give each worker process a stable, unique owner_id; include the host deployment identity and worker slot rather than reusing one value across nodes
  • choose lease_for longer than the maximum expected gap between healthy heartbeats, including scheduler and database latency
  • set heartbeat_interval_ms well below the claim duration so a missed heartbeat does not expire healthy work; the minimum supported interval is 50ms
  • keep queue placement, worker count, restart policy, back-pressure, and shutdown behavior in the host supervision and deployment layers

Concurrent Jizoku.execute_next/1 calls may observe the same visible attempt, but only one claim append can win the dispatch-thread revision fence. A current heartbeat extends that winner's lease and prevents reclaim. After the lease expires, another owner may append a fresh claim and execute the attempt. The old claim token is then stale: later completion or failure from the old owner is rejected before it can mutate dispatch or workflow state.

Cancellation and terminal failure or completion add a run-level fence. Once a run is terminal, later claims and stale worker results cannot reopen or change the terminal state.

Operators can inspect this boundary without parsing journal entries:

  • Jizoku.inspect_run/2 exposes the current owner_id, claim_id, and lease_until in claimed attempts
  • an expired lease moves the attempt into expired_claims and sets the snapshot reason to :expired_claim
  • Jizoku.explain_run/2 reports :recover_expired_claim while takeover is pending
  • after cancellation, failure, or completion, explanation reports :terminal and :inspect_terminal_run instead of suggesting recovery

These guarantees fence Jizoku's durable workflow mutations. They do not make external side effects exactly once. A worker can perform an external action, lose its lease before recording completion, and cause a takeover worker to perform that action again. Side-effecting steps must therefore use stable idempotency keys, domain-level duplicate detection, or compensating actions.

The minimal host app contains the executable shared-storage proof:

cd examples/minimal_host_app
MIX_ENV=test mix test test/multi_node_host_worker_test.exs

It runs distinct node-a and node-b owners against one Postgres-backed journal queue and covers claim contention, heartbeat renewal, expired takeover, stale completion and failure, operator evidence, and terminal fencing.

Hosts using Bedrock Job Queue can verify the separate delivery-lease boundary through the Bedrock example:

cd examples/bedrock_minimal_host_app
MIX_ENV=test mix test test/bedrock_multi_node_consumer_test.exs

That proof runs two independently identified Bedrock consumer managers against one queue. It verifies exclusive dispatch, automatic backend lease renewal, continued invisibility after the original lease expires, and completion from a manager whose initial lease snapshot became stale after renewal. Jizoku's journal claim remains a separate fence inside the delivered job.

Cron Payload Contract

Cron starts are the Jizoku.Executor payload boundary. Hosts that already have a scheduler can enqueue Jizoku.Executor.Payload.cron/3 and deliver the stored payload to Jizoku.Runtime.Runner.perform/2:

defmodule MyApp.JizokuCronExecutor do
  @behaviour Jizoku.Executor

  alias Jizoku.Executor.Payload

  def enqueue_cron(_config, workflow, trigger, opts) do
    workflow
    |> Payload.cron(trigger, Keyword.take(opts, [:signal_id, :intended_window]))
    |> enqueue(opts)
  end

  defp enqueue(payload, opts) do
    job = %{payload: payload, queue: queue(), schedule_in: opts[:schedule_in]}

    case MyApp.JobQueue.enqueue(job) do
      {:ok, job} ->
        {:ok, %{job_id: job.id, queue: job.queue, schedule_in: opts[:schedule_in]}}

      {:error, reason} ->
        {:error, reason}
    end
  end

  defp queue do
    :my_app
    |> Application.get_env(__MODULE__, [])
    |> Keyword.get(:queue, :jizoku)
  end
end

The cron callback receives:

  • workflow and trigger - the cron workflow activation target
  • opts[:signal_id] - optional stable scheduler signal id for a cron activation
  • opts[:intended_window] - optional logical schedule window for a cron activation

Return {:ok, metadata} after enqueueing. Metadata is returned to the caller and can be included in host-owned logs or telemetry, so useful values are :job_id, :queue, :worker, and :scheduled_at.

The queued job should deliver the stored payload back to Jizoku without knowing workflow details:

defmodule MyApp.JizokuJob do
  def perform(%{payload: payload}) do
    Jizoku.Runtime.Runner.perform(payload)
  end
end

MyApp.JobQueue is intentionally a placeholder. In a real host app, replace it with the app's durable job backend. Cron activation is host-owned; the host scheduler should call enqueue_cron/4 or enqueue Jizoku.Executor.Payload.cron/3.

When a scheduler can provide deterministic schedule metadata, pass it with the cron payload instead of adding it to workflow input:

Payload.cron(MyApp.Workflows.DailyStandup, :daily_standup,
  signal_id: "daily-standup:2026-05-15T09:00:00Z",
  intended_window: %{
    start_at: "2026-05-15T09:00:00Z",
    end_at: "2026-05-15T10:00:00Z"
  }
)

Jizoku persists this under run.context.schedule before workflow processing. Steps can read it from context.state.schedule, and inspection or explanation surfaces can show the intended window separately from actual worker receive time.

If the workflow declares cron ..., idempotency: :return_existing_run or idempotency: :skip_duplicate, the scheduler identity also becomes the start idempotency key. Duplicate delivery of the same workflow, trigger, and key will not insert a second run. Idempotent cron starts must include signal_id or a complete intended_window; otherwise Jizoku returns {:error, {:missing_schedule_idempotency_key, trigger_name}}.

With the journal default, cron payload delivery through Jizoku.Runtime.Runner.perform/2 starts a journal run and persists the schedule context on the :run_started journal fact. Only cron payloads are accepted because step execution is claimed through Jizoku.execute_next/1.

That is the whole execution contract for the journal-backed runtime. Workflow modules, context modules, and controllers should not need to know which job backend the scheduler uses.

Optional Lease Contract

Backends that expose worker leases can also implement Jizoku.Executor.Leases. This is separate from the queue delivery adapter: it claims visible work, heartbeats active claims, completes delivered work, and returns failed work to the backend's retry or dead-letter policy.

The journal-backed runtime does not require a lease adapter. The behavior exists so Bedrock or another durable backend can expose lease semantics through a stable Jizoku boundary without changing workflow modules.

Bedrock Lease Backend Setup

Jizoku stays backend-neutral: workflow modules and runtime state do not depend on Bedrock APIs. For hosts that want backend-owned leasing today, Bedrock is the recommended reference backend because it already owns durable delivery, delayed visibility, leases, heartbeats, retry timing, and recovery. That same ownership model is also a better foundation for distributed workflows, where multiple workers may claim, heartbeat, fail, or recover work across process and node boundaries.

Use examples/bedrock_minimal_host_app as the concrete setup guide. The example keeps the storage and lease boundaries explicit:

  • BedrockMinimalHostApp.Repo stores Jizoku workflow and attempt state.
  • BedrockMinimalHostApp.JobQueue stores queue items, delayed visibility, leases, retries, and queue metadata.
  • BedrockMinimalHostApp.JizokuDeliveryAdapter adapts cron activations to Bedrock Job Queue payloads.
  • BedrockMinimalHostApp.JizokuLeaseAdapter adapts Bedrock claims, heartbeats, completion, and failure to Jizoku.Executor.Leases.
  • BedrockMinimalHostApp.Jobs.JizokuPayload delivers cron payloads and then drains visible journal attempts while the Bedrock lease is held.

There are two independent lease layers in that setup. The Bedrock lease belongs to the host job backend and controls whether the payload job can be redelivered. The Jizoku journal claim lease belongs to Jizoku.execute_next/1 and controls whether another workflow worker can reclaim a journal attempt. The Bedrock example passes journal_heartbeat_interval_ms into execute_next/1 so long-running journal steps keep their Jizoku claim alive while the Bedrock payload job is executing. That option does not renew the Bedrock job lease; the host backend must size and renew its own lease separately.

The payload worker is the executor boundary. It should deliver a Jizoku payload, then drain visible journal attempts with Jizoku.execute_next/1. Do not enqueue one Bedrock job per workflow step. Do not use Bedrock job retry settings to represent workflow step retry policy. Step retry, terminal failure, pause, approval, and compensation routing are Jizoku runtime facts driven by the workflow DSL and persisted by execute_next/1.

Treat {:ok, snapshot} from execute_next/1 as successful job progress even when the snapshot reports a failed workflow run. Return {:error, reason} to Bedrock only when the payload delivery or journal drain itself failed and should be redelivered by the backend.

A host app using the same shape should:

  1. Configure :jizoku with the host repo and journal queue.
  2. Configure the cron adapter's Bedrock queue id and topic.
  3. Start the host repo, Bedrock cluster, and Bedrock job queue under supervision.
  4. Keep workflow definitions backend-neutral; only the Bedrock adapter modules should know Bedrock exists.
  5. Configure both lease policies explicitly: Bedrock job lease duration for payload delivery, and journal_heartbeat_interval_ms for long-running Squid Mesh attempts.

The example config shape is:

config :my_app, MyApp.JizokuDeliveryAdapter,
  queue_id: "tenant_a",
  topic: "jizoku:payload"

config :jizoku,
  repo: MyApp.Repo,
  queue: "tenant_a"

config :my_app, MyApp.Jobs.JizokuPayload,
  journal_heartbeat_interval_ms: 10_000,
  max_journal_attempts: 50

To verify the reference path locally:

cd examples/bedrock_minimal_host_app
mix setup
MIX_ENV=test mix test test/bedrock_job_queue_stress_test.exs test/bedrock_minimal_host_app/jizoku_lease_adapter_test.exs

That test path covers Bedrock queue behavior plus the lease adapter contract. It does not make Bedrock a required Jizoku dependency; another durable delivery adapter can use the same Jizoku boundaries if it provides equivalent delivery, lease, heartbeat, retry, and recovery semantics.

For background on why durable workflow systems often benefit from queueing close to the data and tenancy model they serve, see Apple's QuiCK: A Queuing System in CloudKit paper.

First Run Checklist

For a new integration, the shortest path to a successful first run is:

  1. Add :jizoku to the host app's dependencies.
  2. Add or confirm a working Postgres-backed Repo.
  3. Run mix jizoku.install.
  4. Run mix ecto.migrate.
  5. Configure :jizoku with the host app's Repo.
  6. Start the host app's Repo under supervision.
  7. Start one workflow through the public API, execute visible attempts with Jizoku.execute_next/1, and inspect it with history enabled.

Add a host job system only when the app needs one for cron scheduling, backend-owned leases, or other application work.

Existing Application Setup

For an existing Phoenix or OTP application:

  1. Add the :jizoku dependency.
  2. Configure :repo to point at the app's existing repo.
  3. Call Jizoku.config!/0 during boot or integration setup to verify the required contract is present.
  4. Integrate Jizoku from the host application's contexts, services, controllers, or internal APIs.

The host application is responsible for:

  • database setup and migrations
  • journal worker lifecycle for Jizoku.execute_next/1
  • any HTTP or internal API endpoints exposed to end users

That means the embedded install path assumes:

  • the host app already owns its Repo
  • the host app starts workers that call Jizoku.execute_next/1
  • the host app adds job-backend tables only for its own scheduler or lease backend

Minimal OTP Host Skeleton

For a plain OTP application, the minimum moving pieces are:

  • a Repo module
  • Repo in the application supervision tree
  • a supervised worker that periodically calls Jizoku.execute_next/1
  • :jizoku configuration pointing at that Repo
  • one host-facing module that calls Jizoku

Dependency shape:

defp deps do
  [
    {:ecto_sql, "~> 3.13"},
    {:postgrex, "~> 0.20"},
    {:jizoku, "~> 0.3.7"}
  ]
end

Add :jido only when the host app defines raw Jido.Action steps directly. Add the host job backend separately.

Application supervision shape:

children = [
  MyApp.Repo,
  MyApp.JobQueue
]

Host-facing boundary:

defmodule MyApp.WorkflowRuns do
  def start_payment_recovery(payload) do
    Jizoku.start(MyApp.Workflows.PaymentRecovery, :payment_recovery, payload)
  end

  def inspect_run(run_id) do
    Jizoku.inspect_run(run_id, include_history: true)
  end

  def resume(run_id, attrs \\ %{}) do
    Jizoku.resume(run_id, attrs)
  end

  def approve(run_id, attrs) do
    Jizoku.approve(run_id, attrs)
  end

  def reject(run_id, attrs) do
    Jizoku.reject(run_id, attrs)
  end
end

If the host app exposes pause-resume or approval workflows, keep the latest Jizoku migrations applied before deploying the feature. Paused step runs now persist internal resume metadata so resume/2, approve/3, and reject/3 can continue with stable output and transition semantics after restarts or code changes.

Operational review shape:

{:ok, paused_run} = MyApp.WorkflowRuns.inspect_run(run_id)

Enum.map(paused_run.audit_events, &{&1.type, &1.step})
#=> [{:paused, :wait_for_review}]

{:ok, _run} =
  MyApp.WorkflowRuns.approve(run_id, %{
    actor: "ops_123",
    comment: "customer verified",
    metadata: %{ticket: "SUP-42"}
  })

{:ok, completed_run} = MyApp.WorkflowRuns.inspect_run(run_id)

Enum.map(completed_run.audit_events, &{&1.type, &1.actor, &1.comment})
#=> [{:paused, nil, nil}, {:approved, "ops_123", "customer verified"}]

include_history: true is the public audit boundary. With history enabled, the run includes chronological step_runs, declared steps state, and durable audit_events for pause, resume, approval, and rejection actions.

Minimal Phoenix Host Skeleton

A Phoenix application uses the same runtime contract. The main difference is that Jizoku usually sits behind a context or controller boundary.

Typical shape:

  • add :jizoku to the Phoenix app
  • keep using the Phoenix app's existing Repo
  • start a supervised worker that calls Jizoku.execute_next/1
  • configure :jizoku to use that Repo
  • expose workflow operations through a context or controller

Add :jido explicitly only when the Phoenix app defines raw Jido.Action modules as an interop path.

Context boundary:

defmodule MyApp.WorkflowRuns do
  def start_payment_recovery(attrs) do
    Jizoku.start(MyApp.Workflows.PaymentRecovery, :payment_recovery, attrs)
  end

  def inspect_run(run_id) do
    Jizoku.inspect_run(run_id, include_history: true)
  end

  def resume(run_id, attrs \\ %{}) do
    Jizoku.resume(run_id, attrs)
  end

  def approve(run_id, attrs) do
    Jizoku.approve(run_id, attrs)
  end

  def reject(run_id, attrs) do
    Jizoku.reject(run_id, attrs)
  end
end

Controller shape:

def create(conn, params) do
  with {:ok, run} <- MyApp.WorkflowRuns.start_payment_recovery(params) do
    json(conn, %{id: run.run_id, status: run.status})
  end
end

Development Setup

For local development and examples, a minimal host app can provide:

  • a local Postgres-backed repo
  • a local background job setup
  • direct application code calls into Jizoku

This uses the same configuration contract as an existing application setup. In that mode, the example app may also own its job-backend migrations because it is acting as a standalone development harness rather than an embedded install.

Validation

Host applications can validate the contract directly:

{:ok, config} = Jizoku.config()

Or raise on missing required keys:

config = Jizoku.config!()

Example Development Harness

The example host app smoke-test harness builds on this same contract and is the reference setup for end-to-end development and verification.

Path:

  • examples/minimal_host_app

Suggested workflow:

  1. Start Postgres for the example app.
  2. Run mix setup inside examples/minimal_host_app.
  3. Run mix example.smoke to exercise the host app boundary.

Fast verification path:

  • run MIX_ENV=test mix example.smoke inside examples/minimal_host_app

The example app wires:

  • its own MinimalHostApp.Repo
  • journal runtime smoke paths that use inferred Ecto storage and Jizoku.execute_next/1, including cron activation through the journal runtime
  • a Jido command-signal round trip that proves durable trace lineage across a worker handoff and captures a committed lifecycle telemetry event
  • a versioned graph mutation with dependency chain and fan-in readiness, injected post-commit dispatch failure, explicit reconciliation, redacted inspection, and terminal completion
  • cron activation smoke paths that deliver Jizoku.Executor.Payload.cron/3 through Jizoku.Runtime.Runner.perform/1
  • Jizoku through MinimalHostApp.WorkflowRuns

Inspecting History

For real host apps, inspect_run/2 is most useful with history enabled:

Jizoku.inspect_run(run_id, include_history: true)

That returns the top-level run plus:

  • steps: logical per-step state in workflow order, including dependency edges
  • step_runs: persisted execution history
  • attempts: persisted retry history for each step run

This split gives host apps both declared per-step state and the raw execution timeline from one inspection call.

Use explain_run/2 when an operator surface needs the current reason and safe next actions instead of the full inspection snapshot:

{:ok, explanation} = Jizoku.explain_run(run_id)

%{
  status: explanation.status,
  reason: explanation.reason,
  step: explanation.step,
  next_actions: explanation.next_actions
}

inspect_run/2 answers "what persisted state exists?". explain_run/2 answers "why is this run here, what evidence supports that, and what can an operator do next?". The explanation keeps details and evidence structured so Phoenix apps, CLIs, and dashboards can render their own messages.