← All guides

Reasoning, memory and planning — composing the cognitive operations

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.

0. One workflow, one loop

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

1. Remember: submit and beliefs

Your 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': …}, …]

2. Ask: query

query 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.

3. Prove: prove

prove 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.

4. Evaluate evidence: evaluate

The 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.

5. Plan: plan

pl = 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.

6. Observe: observe

ob = 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.

6b. Ask in English: ask

wf.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.

7. Act under a policy: check

For 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.

8. The loop, together

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.

What each operation can and cannot do

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.