← All guides

Persistent workflows — what a workflow keeps, and what it does not

A workflow is your application's handle on one EARL runtime. It binds a domain (your corpus, trained), owns one runtime image, and appends one signed record per call. Two workflows on the same domain share nothing at run time. This page says exactly what persists between calls, how to see it, and where the edges are. Everything here was measured by the P5 battery through the SDK on engine rc4; unchanged on rc5 (SHIP-1), which adds belief provenance (asserted_source) and the execution path on every record.

The model

create_workflow(domain)  ->  init: the trained store + substrate graph are copied into the workflow's runtime
call n   (tick / submit / prove / plan / observe / beliefs / evaluate ...)
call n+1 reads what call n wrote — each call is its own process; the image on disk is the only carrier

Every stateful call: load the image → act → save to a temporary image → read it back and verify → swap it in → sign a record that names the image hash before and after. If the readback fails, the previous image is intact and the call is an OperationalError (write_verification_failed); nothing half-written is ever kept.

What persists

state written by read by measured
the belief store (typed propositions with confidence) init, submit, the cognitive cycle (tick) beliefs, query, prove P5 I3a/I3b/I3b1/M1
the cognitive cycle counter and the clock domain tick observe (total_cycles), every later call I3a, I3c: tick 3 then tick 1 reads cycle 3; census 0 → 5
the substrate graph the runtime grounds on (graph.belf) init (copied from the trained checkpoint), submit (accreted) submit rc4
the record chain every call records(), record(id), replay I6a–I6e
the pinned engine release (A1) adopt_engine / rollback_engine every later call I9a–I9c
(rc27) text staged for self-training and reported outcomes, until the next tick consumes them submit with self_training on, report_outcome tick engine tests; rc27 gate
(rc27) the self-trained vocabulary and the learner's outcome ledger tick learning(), every later tick engine tests; rc27 gate
(rc27) derived beliefs with their premises, and the deduction snapshots deduce(confirm=) beliefs() (derived_from), rollback_deduction engine tests; rc27 gate

What does not persist (by design, in M1)

memory.submit: the input boundary, stated

submit(text) runs the same extractor training uses over the workflow's copy of the trained substrate. It forms a typed belief only for a pair of grounded concepts the domain vocabulary links (a band that contains both) — the training rule. The record tells you what happened:

field meaning
submitted true if the call changed the store: at least one grounded relation was newly asserted (beliefs_formed, propositions) or re-observed (beliefs_reinforced, reinforced). It does not mean a new belief was admitted. A text whose only grounded relations were already held reads submitted: true, beliefs_formed: 0. Read propositions and reinforced for what changed, and unlinked_pairs for what the text stated but could not form
entities, relations what the extractor found
propositions the new typed beliefs (e.g. premier requires flood)
reinforced grounded relations the store already held; re-observed, no duplicate
reason: no_belief_formed nothing grounded; the call is SemanticUncertainty; the image is unchanged
unlinked_pairs (rc25 on) pairs the text stated (a verb between them in one clause) that formed nothing because the domain's vocabulary does not link them, e.g. [{"from": "pump", "to": "approval"}]. Put both in one vocabulary band and rebuild the domain KB to make the pair learnable
explanation (rc25 on, when nothing formed) the reason in words, naming the first unlinked pair and the fix

A submitted proposition is listed by beliefs() in a later call, survives later ticks, and feeds query (yesno premier requires flood derives). Persistence is not truth: what persists is what the extractor read. The open extraction defects apply to your text exactly as they apply to a corpus — a reversed pair across consecutive same-verb sentences (F16), a base-KB multiword token (F17), passive voice (F18), a verb read as a negation (F23). Read propositions back before relying on them.

Runtime learning controls (rc27, gated)

Three learning paths are off until you switch them on for the workflow (set_learning_controls(...); each change is a signed record; switching on needs an rc27 or later engine):

switch what it enables what the record shows
self_training submit also stages its text (code and identifiers removed, from rc30) for EARL's own corpus induction; the next tick inducts over the staged text and the last 32 documents, rejecting identifiers, number-word compounds and WordNet adjectives/adverbs submit: staged_for_induction, staged_doc_id, staging_removed; tick: words_minted, words_rejected (from rc44, every candidate left unlearned, each with its reason: corpus glue, too few documents, too few known neighbours, below the threshold), words_associated, associations_made (from rc46: every association the tick created, with the notes in which the pair co-occurs), learning_before / learning_after; learning(): every self-trained word with the submit records it came from, and from rc44 every association self-training holds (associations, each with the tick records and submit records that made it; from rc46 the submit records are those of the notes in which the pair co-occurs, documents_are: "pair", for learned and known words alike, and a tick from an older engine names the notes the word is in, marked "word") and what the last learning tick passed over (passed_over, with reasons)
outcome_learning report_outcome(record_id, succeeded, confidence=, tokens=) tells the learner what happened after an earlier result of this workflow; the next tick attributes it outcome: outcome_of (the verified record), the words, pending_outcomes; tick: the learner's outcome ledger before and after
deduction deduce() previews what the closure derives (fact, rule, premises, digest) with each premise's sources and trust; deduce(confirm=digest) writes the trusted conclusions (premises taught or operator-verified) after a snapshot, untrusted ones only if named; rollback_deduction(n) restores the image byte for byte preview: derivations, digest; confirm: deduced with premise_ids, snapshot; beliefs(): derived_from

Switching self_training or outcome_learning off drops anything that path staged and the next tick has not yet consumed (engine rc28; on rc27 the switch record says the staged input is still learned).

The engine decides when it learns: a staged text or an outcome takes effect on the next tick, never in the call that supplies it. An outcome names no pattern, outcome type or weight; the learner attributes it. A confirmed deduction is refused if the closure changed since its preview, and a rollback is refused if the workflow changed after the deduction. None of the three is qualified: read what each record says it did.

reason.prove and reason.query

prove(query) is a Result only when the engine reports status: proved (chain closed, every step justified). incomplete_derivation and not_derivable are SemanticUncertainty: a withheld derivation, never a proof.

From rc25, prove proves from the workflow's own beliefs. - A held fact (taught, trained or submitted) proves in one step that cites its belief. - Chains prove step by step: transitivity, duality, symmetry, inheritance from a kind. - steps lists each fact with how it is justified; derivable is the closure's verdict. - See reasoning-and-planning.

Up to rc24 the prover was given none of the workflow's beliefs, so a held fact returned incomplete_derivation with 0 steps (register row reason.prove).

query(line) derives ontology relations (requires, enables, special_case_of, part_of, causes, prevents) over the runtime's typed triples with the explicit grammar (yesno s p o, whatis x, whatneeds x, related x, ...).

Concurrency and idempotency

Stateful calls on one workflow are serialized by a per-workflow lock; a second caller waits, then either runs or gets WorkflowBusy (409) after 30 s. An idempotency_key (the SDK derives one from the inputs for evaluate) returns the existing record instead of re-running the engine.

Isolation

Runtime directories are per workflow; another workflow on the same domain starts at cycle 0 with the trained store only (I4a). Another organisation gets NotFound on your workflow, records, packages and replay, and cannot create a workflow on your domain (I4b/I4c).

Engine releases (A1) — not successors (A2)

adopt_engine(tag) pins an installed release for the workflow; the image is not touched; the next call runs on the adopted binaries and its record names them. Adopting a release that lacks a binary makes the next call an OperationalError engine_binary_missing; rollback_engine() restores the previous release and the state continues. Adoption of an EARL-authored successor runtime (A2) is a different contract, gated on mode C, and is not in this release.

Which engine ran an operation. The authoritative identity is on the operation's own record (identity.binary_sha256, engine_release_tag). The workflow object's runtime_identity (GET /v1/workflows/{id}) is the identity recorded at init and does not follow a later engine_adopt; after an engine adoption or rollback it can name a binary the workflow no longer runs on (R4 review 2026-10-11, finding F4). Read the records, not the workflow field, when it matters which engine executed a call. Note also that some reads write: GET …/development/lineage and GET …/goals/allocation run the engine and append a signed record to the workflow, so a record count quoted anywhere is a snapshot (finding F6).

When something goes wrong

record says what happened what to do
operational_error runtime_image_rejected the image on disk failed its checks at load the previous verified image is what is on disk; the call did not save; investigate the storage; a new workflow is the safe recovery
operational_error engine_binary_missing the pinned release lacks the binary rollback or adopt
semantic_uncertainty the mechanism ran and declined more evidence or a different question; not a retry
record fails verify_offline the record was altered or is not from this platform's issuer do not trust it