This document is a Diataxis "how-to guide": problem-oriented recipes for an application
that depends on the ex4pm (or ex4pm_core) hex package, not for ex4pm's own
contributors. Every recipe cites a real public function on the Ex4pm module or a
sibling public module. No internal Mix tasks, umbrella-only tooling, or
qualification-suite commands appear here — those are for ex4pm's own repo, not for a
downstream consumer.
How to add ex4pm as a dependency
Local development: path dependency
While iterating against a local checkout of ex4pm (e.g. ~/ex4pm next to your app),
point mix.exs at the path directly:
defp deps do
[
{:ex4pm_core, path: "../ex4pm/apps/ex4pm_core"}
]
endPath dependencies recompile whenever the referenced source changes, so this is the right mode for co-developing your app against ex4pm changes that haven't been published yet.
Once published: hex dependency
Once ex4pm_core (or ex4pm) is published to Hex, switch the same entry to a version
requirement:
defp deps do
[
{:ex4pm_core, "~> 0.1"}
]
endRun mix deps.get after changing either form. Only change one consumer app's
mix.exs at a time when migrating from path to hex — verify mix deps.get and a
compile succeed before removing the path form, so you always have a working
dependency declaration to fall back to.
How to validate an OCEL v2 envelope before sending it elsewhere
Before forwarding an OCEL v2 batch envelope (events, objects, object relationships) to
another system, validate its shape with Ex4pm.OCEL.validate_envelope/1:
envelope = %{
"schema" => "ocel2",
"producer" => "my_app",
"sequence" => 1,
"events" => [
%{"id" => "e1", "type" => "order_placed", "time" => "2026-09-09T10:00:00Z"}
],
"objects" => [
%{"id" => "o1", "type" => "order"}
],
"object_relationships" => []
}
case Ex4pm.OCEL.validate_envelope(envelope) do
{:ok, normalized_envelope} ->
# normalized_envelope has canonical key/value shapes; safe to forward
send_to_downstream(normalized_envelope)
{:error, %Ex4pm.Refusal{} = refusal} ->
# refusal.code / refusal.message / refusal.details explain what failed
Logger.warning("OCEL envelope refused: #{refusal.message}")
endvalidate_envelope/1 returns the normalized envelope map on success, or a typed
Ex4pm.Refusal struct (never a raw error string or exception) on a malformed
envelope. Match on {:error, %Ex4pm.Refusal{}} rather than assuming any particular
exception type.
If you instead need to normalize a raw OCEL-v2-like payload into ex4pm's canonical
Ex4pm.EventLog IR (rather than just validating a batch envelope), use
Ex4pm.ingest/2:
{:ok, event_log} = Ex4pm.ingest(raw_ocel_map)How to call Ex4pm.operate/3 with an explicit authority map
Ex4pm.operate/3 is the only entry point that crosses into state-changing execution
(DO). It requires a POWL model or a compiled Ex4pm.Runtime.Plan, plus an explicit
authority map — there is no ambient authority, so a call without one is refused, not
silently permitted.
{:ok, powl_model} =
Ex4pm.POWL.new(
[%Ex4pm.POWL.Task{id: :ship, label: "Ship order", intent: :ship_order}],
[]
)
authority = %{
granted_operations: [:ship_order],
principal: "order_service"
}
case Ex4pm.operate(powl_model, authority, []) do
{:ok, %{plan: plan, execution: execution, standing: standing}} ->
# standing is the evidence-standing atom (e.g. :alive, :blocked) for this DO
handle_execution(execution, standing)
{:error, %Ex4pm.Refusal{} = refusal} ->
Logger.error("operate refused: #{refusal.message}")
endThe exact shape of the authority map (which keys it must carry to grant a given
operation) is defined by Ex4pm.Evidence.BRCE.admit/2, which operate/3 calls
internally — build your authority map to match the operations your POWL tasks declare
via their intent field. A call with an authority map that doesn't grant the task's
operation returns a refusal rather than executing partially.
How to replay a receipt via Ex4pm.replay/2
Every analytical operation (discover/2, conform/3, simulate/2, optimize/3,
plan/2, cmca/2) and every operate/3 call produces a receipted evidence trail. To
independently verify a receipt's hash and standing later — without trusting the
original invocation — call Ex4pm.replay/2 with the receipt hash:
{:ok, run} = Ex4pm.discover(event_log, [])
receipt_hash = run.receipt.hash
case Ex4pm.replay(receipt_hash, []) do
{:ok, verified_receipt} ->
# the hash was independently recomputed and matches; standing is confirmed
verified_receipt.standing
{:error, %Ex4pm.Refusal{code: :receipt_not_found}} ->
Logger.warning("no receipt found for hash #{receipt_hash}")
{:error, %Ex4pm.Refusal{} = refusal} ->
# e.g. :invalid_receipt or :replay_mismatch — the chain failed to verify
Logger.error("replay failed: #{refusal.message}")
endPass store: MyApp.CustomStore in the opts keyword list if your app uses a receipt
store other than the default Ex4pm.Evidence.Store.
How to check which DfCM engine candidates are available at runtime
Different operations (:discover, :conform, :simulate, :optimize, :plan,
:cmca) may have multiple candidate engines (BEAM-native, WASM, a pinned remote
planner, a NIF, etc.), each with its own evidence standing. Before relying on a
specific engine being available, inspect the candidates with Ex4pm.capabilities/2:
candidates = Ex4pm.capabilities(:discover, [])
Enum.each(candidates, fn capability ->
IO.puts("#{capability.id}: #{capability.standing} (#{capability.reason})")
end)capabilities/2 takes the operation as its first argument (defaults to :discover)
and returns a list of Ex4pm.Core.Capability structs, evidence-ranked so the
highest-standing candidate (e.g. :alive over :partial_alive over :blocked) comes
first. Use this to decide whether to proceed with a default engine selection, prompt
for a fallback, or surface a warning to your own users when only a degraded engine is
available for an operation you depend on.
How to run a differential comparison between two engines
To compare two candidate engines' results for the same operation and subject — for
example, validating that a new WASM engine agrees with the established BEAM-native
engine before switching your app's default — use Ex4pm.differential/5:
result =
Ex4pm.differential(:discover, event_log, :beam, :wasm_discover, [])
case result do
{:ok, comparison} ->
# comparison carries both engines' results/standings and their agreement
inspect(comparison)
{:error, %Ex4pm.Refusal{} = refusal} ->
Logger.error("differential comparison refused: #{refusal.message}")
endThe five positional arguments are: the operation atom, the subject (a log, model, or
problem map appropriate to that operation), the left engine id, the right engine id,
and an opts keyword list. Use Ex4pm.capabilities/2 first to confirm both engine ids
you want to compare are actually registered candidates for that operation.
How to mine your own CI/scheduled-task pipeline for throughput and conformance
If you run a CI or scheduled-task pipeline that qualifies commits, runs a crown/release process, merges the result, and occasionally repairs a failure, you can model each of those pipeline events as an OCEL v2 event and use ex4pm to discover the pipeline's real directly-follows behavior and check it against an expected model — rather than manually cross-referencing scheduler logs against merge timestamps by hand.
Model each event with event_type one of "commit_qualified", "crown_run",
"merge", or "rca_repair", and object_type "batch" (or "lane" if you track
concurrent lanes separately). First, ingest a known-good lane — one where a
commit_qualified run was always followed by a crown_run and then a merge — and
discover its directly-follows graph with Ex4pm.discover/2:
expected_raw = %{
"objects" => %{
"batch-good" => %{"type" => "batch"}
},
"events" => %{
"e1" => %{
"activity" => "commit_qualified",
"timestamp" => "2026-09-09T09:00:00Z",
"objects" => ["batch-good"]
},
"e2" => %{
"activity" => "crown_run",
"timestamp" => "2026-09-09T09:05:00Z",
"objects" => ["batch-good"]
},
"e3" => %{
"activity" => "merge",
"timestamp" => "2026-09-09T09:10:00Z",
"objects" => ["batch-good"]
}
}
}
{:ok, expected_log} = Ex4pm.ingest(expected_raw)
{:ok, expected_model} = Ex4pm.discover(expected_log, object_type: "batch")expected_model.value is the real discovered directly-follows graph — a map with
edges keyed by {from_activity, to_activity} tuples, plus starts, ends, and
activities counts:
%{
type: :dfg,
edges: %{
{"commit_qualified", "crown_run"} => %{count: 1, average_duration_ms: 300_000.0},
{"crown_run", "merge"} => %{count: 1, average_duration_ms: 300_000.0}
},
object_type: "batch",
activities: %{"commit_qualified" => 1, "crown_run" => 1, "merge" => 1},
ends: %{"merge" => 1},
starts: %{"commit_qualified" => 1},
trace_count: 1
}Now ingest the pipeline's actual behavior for a batch that ran commit_qualified and
crown_run but never merged — the scheduler retried the qualifying commit instead —
and check it with Ex4pm.conform/2 against the expected model:
actual_raw = %{
"objects" => %{
"batch-stalled" => %{"type" => "batch"}
},
"events" => %{
"f1" => %{
"activity" => "commit_qualified",
"timestamp" => "2026-09-09T10:00:00Z",
"objects" => ["batch-stalled"]
},
"f2" => %{
"activity" => "crown_run",
"timestamp" => "2026-09-09T10:05:00Z",
"objects" => ["batch-stalled"]
},
"f3" => %{
"activity" => "commit_qualified",
"timestamp" => "2026-09-09T10:35:00Z",
"objects" => ["batch-stalled"]
}
}
}
{:ok, actual_log} = Ex4pm.ingest(actual_raw)
{:ok, conformance} = Ex4pm.conform(actual_log, expected_model.value, object_type: "batch")
conformance.value.fitness
# => 0.5
conformance.value.deviations
# => %{{"crown_run", "commit_qualified"} => 1}The crown_run -> commit_qualified deviation is the real signal: this batch's crown
ran, but instead of the expected merge within the modeled window, the pipeline looped
back into another qualifying-commit attempt — an expected merge event absent where the
model says one should follow. That is the concrete mechanism that turns a stalled,
unmerged batch into a conformance violation Ex4pm.conform/2 surfaces automatically,
instead of requiring a human to manually diff scheduler logs against merge timestamps
after the fact. Widen the expected model (ingest more known-good lanes before calling
Ex4pm.discover/2) to admit legitimate variations — such as a rca_repair step between
crown_run and merge — without losing the check on the pipeline stalling out
entirely.
See Also
Ex4pm.Contracts.verify/0(viaEx4pm.contracts/0) — fetch and verify the ontology/SHACL/WIT/receipt-schema contract manifest and its combined hash, useful as a codegen source of truth (e.g. forggen) rather than hand-copying schema files.Ex4pm.Refusal— the typed refusal struct returned by every function in this guide on failure; match on%Ex4pm.Refusal{code: ..., message: ..., details: ...}rather than a generic error tuple or exception.Ex4pm.OCEL2— derives object traces, attribute-change history, and object relationships from an already-normalizedEx4pm.EventLog, useful for drill-down views over data ingested viaEx4pm.ingest/2.