How an application uses more than one capability on one workflow: submit what it learns, ask what follows, prove a chain, plan a goal, observe the state, and evaluate claims against evidence — in one loop your code owns. The persistent workflows guide says what the runtime keeps; this one shows the calls composed, with the outcome each one can return.
The example is a journal-entry review service (the reference vertical in the package): entry types, approvers and exceptions. Substitute your own nouns.
from earl_sdk import EarlClient, Result, SemanticUncertainty
client = EarlClient(PLATFORM_URL, API_KEY)
wf = client.application(APP_ID).workflow() # persistent runtime over the trained domain
submit and beliefsYour service learns something at run time — a policy note, a resolved exception — and tells the runtime:
sub = wf.submit("A revenue entry requires a treasurer approval.")
if isinstance(sub, Result) and sub.result["submitted"]:
print(sub.result["propositions"]) # ['revenue requires treasurer']
else:
print(sub.outcome_class, getattr(sub, "reasons", None)) # no_belief_formed: nothing grounded
submit forms typed beliefs only for pairs the domain vocabulary links; an ungrounded text is an explicit
non-success, never a silent no-op. The belief is in the store for every later call and after restarts:
wf.beliefs("revenue")["beliefs"] # [{'proposition': 'revenue requires treasurer', 'confidence': …}, …]
queryquery derives ontology relations over the runtime's typed triples with an explicit grammar:
wf.query("yesno revenue requires treasurer") # Result: {'answer': 'yes', ...} — the belief you submitted
wf.query("whatneeds revenue") # what a revenue entry requires
wf.query("related treasurer")
Modes: yesno s p o, whatis x, whatneeds x, related x. A question the store cannot answer is a
SemanticUncertainty with its reason, not a guess.
proveprove proves a claim from what the workflow holds (engine rc25 on). It returns a Result only when the
proof closes with every step justified.
pf = wf.prove("manual requires treasurer") # or "Does a manual require a treasurer?"
if isinstance(pf, Result):
print(pf.result["status"]) # 'proved'
for step in pf.result["steps"]: # each step: a cited belief, or a rule over earlier steps
print(step["fact"], "-", step["by"], step["belief_id"])
# manual requires cash - held 41
# cash requires treasurer - held 57
# manual requires treasurer - transitivity of requires (steps 1-2) None
else:
print(pf.reasons) # ['incomplete_derivation'] or ['not_derivable']
How a step is justified:
- held: a belief the workflow holds (taught, trained or submitted), cited by belief_id and confidence. A fact stated in one sentence proves in one held step.
- A named rule over earlier steps:
- transitivity along held edges (manual → cash → treasurer);
- duality (o enables s gives s requires o);
- symmetry (opposite_of);
- inheritance (a kind's property passes to its members).
What the statuses mean:
- derivable is the reasoning closure's verdict on the claim.
- incomplete_derivation: the closure holds the claim, but no chain of held beliefs and rules explains it. That is withheld, never a false proof.
- not_derivable: the closure doesn't hold the claim. A held denial for the claim is reported in denied_by.
Up to rc24 prove was given none of the workflow's beliefs, so a held fact returned incomplete_derivation
with 0 steps. See the register.
evaluateThe one-shot check of a claim in ordinary English against the trained domain (standalone verifier, read-only):
ev = wf.evaluate("A manual entry requires a controller approval")
claim = ev.result["claim_results"][0] # present on Result AND on SemanticUncertainty
claim["verdict"], claim["endpoints"], claim["predicate"], claim["negated"], claim["asserted_by"], claim["asserted_source"]
A claim whose policy disposition is not ALLOW comes back as a SemanticUncertainty that still carries the
claim results: branch on the outcome class, then read the fields. An explicitly denied pair contests the
affirmative claim (CONTESTED affirmative_claim_negated_in_substrate); an unmentioned pair is
UNCERTAIN relation_unasserted; an unknown word is HALLUCINATED.
planpl = wf.plan("resolve a missing controller approval")
pl.result["steps"] # structural steps with a confidence; stateless in this release
Planning does not persist planner state between calls in this release and its steps are structural; use it to order work your service then carries out with the other operations, not as a decision by itself.
observeob = wf.observe()
ob.result["census"] # counts of the runtime state; read-only, never saved
Snapshot observe() before and after a risky change (a new corpus version, an engine adoption) and diff.
askwf.ask(question) answers a plain-English question from the domain's trained facts — yes/no, lists in both
directions, why, describe, relate and compare — with the interpretation, the asserted facts (taught or derived)
and a faithfulness gate on every answer. It reads the domain's store; it does not change the workflow's runtime
image. "Cannot establish" is not "false"; unfamiliar verbs get a clarification; writing, advice and authorization
requests are declined. See natural-language-questions.
checkFor a decision your software will act on, run the claims as an adjudication and ask the policy:
pkg = wf.adjudicate(["A manual entry requires a controller approval", "A recurring entry requires a treasurer approval"])
decision = wf.check("post-entry", pkg.evidence_package_id) # ALLOW / REVIEW / DENY, every matched rule named
check is advisory: it records the decision; it enforces nothing unless the adjudicator container or a
gateway is the only path to the action.
def review(entry_text: str) -> str:
wf.submit(entry_text) # remember what the entry states
ev = wf.evaluate(f"{entry_text}") # evidence for the claim it makes
claim = ev.result["claim_results"][0]
if claim["verdict"] == "VERIFIED":
proof = wf.prove(claim["asserted_by"]) # does it follow from the policy chain?
return "post" if isinstance(proof, Result) else "review: verified but not derived"
if claim["verdict"] == "CONTESTED":
return "reject: " + claim["demotion_reason"]
plan = wf.plan(f"resolve {claim['demotion_reason']} for {claim['endpoints']}")
return "review: " + "; ".join(s["action"] for s in plan.result["steps"])
Every call in that loop is a signed record on the workflow; wf.records() lists them in order and
client.verify_offline(record) checks any one of them. That is the shape of a stateful application on
EARL: your code decides, the runtime remembers, reasons and shows its evidence.
| operation | returns a Result when |
SemanticUncertainty when |
state |
|---|---|---|---|
submit |
at least one typed belief formed | nothing grounded (no_belief_formed) |
writes the store |
beliefs |
always (may be empty) | — | reads |
query |
the grammar derives an answer | no derivation | reads |
prove |
the proof closes from held beliefs, every step justified (rc25 on) | incomplete_derivation, not_derivable |
reads |
evaluate |
policy ALLOW (or no policy) | any other disposition, with claim results | read-only verifier |
plan |
structural steps produced | — | stateless |
observe |
always | — | read-only |
check |
ALLOW | REVIEW / DENY | records only |
The register (capability limitations) states the qualification and limits of
each operation; evidence.evaluate is not qualified for unattended approval decisions.