How a case moves
A case is a question about a borrower that somebody has to answer. The engine has one job — make sure that at every moment, either the machine can move the case forward, or a named person has been asked to.
A case runs itself. At every moment the engine either moves it to the next stage or asks a named person to, and it never rests in a state where neither is true. Nothing starts by itself: the Lending Core reports something about a loan, that report is recorded as a signal, and the signal opens a case. From that moment the case belongs to a workflow: an ordered set of steps, written as configuration rather than code, that the case walks until it can be closed with an outcome.
The engine never runs on a timetable of its own. It runs when something happens. A new event on the loan, a completed task, a granted approval, a recorded decision — each one wakes the engine, which looks at where the case stands and asks whether it can move.
Five words, and nothing else to learn
Almost everything in the engine is one of these five things.
| Term | What it is |
|---|---|
| Case | The question about the borrower. It has a type, a status, an owner, and eventually an outcome. |
| Definition | The written flow for one kind of case. Configuration, not code, and versioned. |
| Step | One position in the flow. The case is always standing in exactly one. A step says what work it wants, what has to be true to leave, and where to go next. |
| Task | One unit of work a step asks for. An AI agent takes it and carries it to an outcome; a person takes it where the task type reserves the decision for one. Either way it ends with a recorded outcome, and the outcome is what the next step reads. |
| Fact | Something true about the loan or borrower, read live from the Lending Core or computed by the compliance engine. Facts are what a step’s rules are written about. |
One more, because it explains a lot of behaviour. A visit is one occupancy of one step. A case that reaches a step, leaves, and comes back later has made two visits. Rules a step writes about its own work mean the work of the current visit — so a review completed the first time round cannot answer for a review that was never done the second time.
A signal does not automatically make a case
Every case begins with a signal: a recorded statement that something happened. Most come from the Lending Core. Others come from a borrower’s email or call, from external monitoring, or from a person creating a case by hand.
The signal is checked against the world first, in a fixed order, and only the last branch creates something new. This is what stops one borrower’s missed payment from opening a fresh case every night.
The anti-orphan rule
A case is created and opened in the same command, and its workflow starts before the case is checked for accountability — because a step that needs a person creates that task in the same transaction. Then the engine enforces the anti-orphan rule: an open case must derive an accountable next step. Automation, a documented wait, a named owner, or a real task. A case that derives none of those is rejected outright rather than saved.
That is a strong claim and it is deliberate. A case with nothing on it is not a runtime problem to clean up later. It is a defect in the flow’s configuration, and the platform would rather refuse to create the case than hold a borrower’s matter that nobody will ever look at.
Which flow a case gets
Two inputs, a fixed order, and nothing else participates.
- The reason’s override. A case reason may name a workflow key. A borrower whose hardship is medical goes to the medical-leave flow; every other hardship disclosure goes to the affordability flow. The reason names the key, not a version, because versions turn over and the reason cares which flow it wants rather than which version happened to be current when it was written.
- The case type’s default. Otherwise the type’s configured default, and failing that the active definition named after the type itself.
A reason that needs its own shape gets its own definition, never its own case type. Once chosen, the case is pinned to that exact version for life.
What a step actually is
A step is not a screen and not a piece of code. It is five rules written next to each other, read in a fixed order. Once you can see this shape, the whole engine follows from it.
A sixth field is what an operator sees. A step that waits on the outside world declares a wait status, and the case wears it. That is why a case can read as patiently waiting on the Lending Core rather than as an orphan nobody is working.
Each piece of work a step asks for is conditional. Some conditions are already settled when the case arrives; some can only become true afterwards — a payout that fails, an exception a reviewer records, a request the Lending Core rejects. Both kinds are re-read on every trigger, and each piece of work is raised at most once per visit.
The rules a step is written in
Every rule is a small expression over a fixed list of sources. There is no scripting language, no arbitrary query, and no way for a flow author to write code. If a rule needs something the list below cannot express, it is added as a named condition, reviewed like any other code, and registered by key.
That constraint is the reason a flow can be edited by the business without a release, and the reason a definition can be checked by a machine before it is allowed to go live.
| A rule can ask about | Meaning |
|---|---|
fact |
A value read live from the Lending Core: days overdue, the payout state, the autopay state. Facts used for a contact or money decision are re-read at the moment of execution, never served from a remembered value. |
derivedFact |
A value the servicing side computes: remaining contact capacity, whether a cease restriction applies, which channels are allowed. |
case |
Registered fields on the case itself — status, reason, priority, sensitivity, and a small catalog of code-owned flags. Never tenant-configured extras. |
signal |
Normalised fields from the signal that triggered the case. Never raw provider JSON. |
task |
A task type’s status or its recorded outcome. For a type this step asks for, only this visit’s instance counts. |
decision |
A judgement a named person wrote down, from a closed list. Silence never satisfies a step. |
governance |
Whether the Lending Core has confirmed a specific change was actually made. This is how a step requires that something happened rather than that somebody said it did. |
evidence · approval |
Documents attached, and approvals granted. |
actionRequest · caseEffect |
The outcome of a governed operation, and the status of an effect. |
condition |
The escape hatch: a registered, reviewed condition referenced by a stable key. A definition naming an unregistered key is rejected when it is saved. |
Rules combine with all and any, which nest. They are evaluated in written order and short-circuit, so ordering inside a rule is meaningful. Every rule resolves to one of three answers, not two, and the third is the whole reason the platform is safe to run against real loans.
TRUE
The condition holds. The case may move.
FALSE
The condition does not hold. The case stays where it is.
MISSING
The engine could not find out. It never guesses and never advances. It raises real work asking a person to establish the fact.
What happens when something wakes it
The engine is woken by a case being created or changing status, a task completing or being released, a fact snapshot changing, an action request or effect reaching a result, evidence or an approval arriving, a scheduled sweep, or an operator asking it directly. Whatever the trigger, the same sequence runs against the step the case is standing in.
Each row can end the pass. If the case does move, the sequence starts over from the top, so a case can walk several steps on a single event.
Moving to the next step
Advancing is four things in a fixed order, and the third is the one people do not expect.
Two smaller rules matter when reasoning about a flow. Routes are tried in written order and the first match wins, so a general catch-all placed above a specific branch will swallow it. And a route whose condition is unknown does not advance the case; it raises fact work instead, because the engine will not guess which way to go.
When the flow is overtaken
Some facts make an entire flow moot rather than satisfying the step the case happens to be standing in. A borrower who pays in full has stopped being a collections matter, and which step the case had reached when the money arrived is not a reason to keep working it.
Those are written once for the whole flow as reactions: a condition, the steps it is armed during, and where the case goes. They are evaluated before the step’s own exit logic on every pass, and the first match wins.
Effects: work the platform runs itself
A step can also ask for an effect: work the platform runs itself against the Lending Core or the servicing world, as opposed to a task for a person. Effects are a fixed, code-owned list. A flow author picks one by name and picks its target from two choices. There is no URL, no request body, no header, no script, and no way to name an arbitrary identifier.
| Effect | What it does |
|---|---|
CANCEL_AUTOPAY |
Stops the automatic payment on the loan the triggering signal names. |
RELEASE_PROTECTIVE_STATE |
Lifts the contact blocks this case is holding, and only this case’s. Another open case protecting the same borrower keeps its own. |
REFER_PROTECTIVE_CASE |
Opens the protective successor a case’s own state names, records the origination, applies the block and the authoritative hold, and suppresses outreach. |
OPEN_LEGAL_ACTION_CASE |
Opens the legal matter as its own case. |
How a case ends
A flow’s last step is marked terminal. Reaching it does not close the case by itself. Three things happen in order.
- The outcome is chosen. The definition’s terminal-outcome rules are tried in order and the first match names the case’s outcome: cured, refunded, referred to hardship, arrangement failed, not eligible. This happens before anything that reads the outcome is checked.
- The guardrails are re-checked. No active tasks and no unresolved effects, plus whatever else the terminal step’s own exit demands. This is deliberately belt and braces: the case state machine enforces it too.
- The case resolves and closes, recorded under the system actor.
Ownership does not veto a close. Every judgement the closure depended on was already recorded by whoever made it, on the tasks and decisions carrying their names, and that attribution survives. There is deliberately no “finished, but sitting open because it has an owner” state. Where an owner’s judgement is genuinely still needed, the flow says so with a task or a decision the terminal gate reads, so completion honestly cannot be reached yet.
A closed case is not necessarily final. A qualifying new signal reopens it under the system actor and re-evaluates it. A case closed as a duplicate is never reopened; the signal follows its primary instead. Nor is a collections case — reopening one would resurrect a campaign whose contact budget was already spent, so a fresh delinquency opens a new case that inherits what the old one used.
How a person intervenes
There are four different things a person can act on, and they are not interchangeable. Confusing the task with the case is the most common reason a request turns out not to mean what it sounded like.
A worker who cannot finish the current step’s work has one honest thing to say, and saying it must not strand the case — so the release is treated as a request for recovery rather than as an ending. The task is held, nobody else is offered it, and exactly one recovery review is raised, deliberately sent somewhere other than the team that could not finish the work. Who gets the work covers that lane in full.
Two properties matter when designing around this. Case-level moves are separate from flow evaluation: snoozing, escalating, sending to supervisor review and changing the owner all move the case without re-evaluating its workflow, because none of them is a statement about the work the step is waiting for. A snooze resuming does re-evaluate, and so does closing. And a running case keeps its flow version: there is no command that moves one onto a newer definition.
A manual case has no workflow at all: no steps, no routes, no timers, no effects. Its work is the checklist its people write, and it closes through the same guards as any other case once nothing on it is open. It is where work goes when no route fits, so a person can continue by hand rather than a case being held open by a machine that cannot move it.
When a case cannot move
The engine assumes flows will sometimes be wrong. A configuration is written by a person, and a case that cannot move is a real borrower waiting. So there is a layer underneath whose whole job is to notice, and to put a name against work that has stopped.
| What goes wrong | What catches it |
|---|---|
| A worker cannot finish the step’s work | The recovery lane. The task is held rather than abandoned, and one review goes to somebody who can decide. |
| Something makes the whole flow moot | A reaction, written once for the flow rather than patched into every step it could interrupt. |
| A step is left behind with work still open | Leaving a step withdraws the work it was asking for, with the exit as the audited reason. |
| A case stops moving for any reason | A sweep gives the engine its own chance first, then asks five named questions: has the workflow ended while the case is still open, is the exit satisfied with the case still standing there, is work armed with nothing to show for it, did work end without a continuation, and did an effect fail with no review. Each check is a statement about work the case was owed and does not have — deliberately not a general theory of stuckness, which flags healthy cases and misses real ones. |
| A case is watching and waiting | A standing timer raises its review on a cadence, and new outside evidence pulls that review forward instead of waiting for the next cycle. Between reviews the case reads as scheduled, never as an orphan. |
| A flow loops on itself at runtime | The engine notices it has already been at this step in this pass, records the loop as a defect in the flow rather than spending its budget silently, and gives the owning queue work naming it. |
| The flow itself is edited | Nothing changes for cases already running. |
A refund, all the way through
Rosa Iyer paid her Cedar loan off by direct debit on the same day her cheque cleared. The loan ended up holding money that was hers. Nobody reported it — the Lending Core noticed the credit balance and said so, and that opened a payment investigation.
Here is the whole flow she walks. Two people touch it, deliberately two different people. Everything else is the engine.
Reading one step from the flow
This is the sixth step, as it is written — the whole page in eight lines.
"stageKey": "AWAIT_PAYOUT",
"waitStatus": "WAITING_ON_LMS",
"taskTemplates": [{
"taskType": "REVIEW_REFUND_EXCEPTION",
"autoCreateWhen": { "refundPayoutState" is one of
FAILED, REJECTED, POSTED_FAILED_REVIEW, CANCELLED }
}],
"exitCriteria": { "refundPayoutState" is POSTED },
"next": [ then go to "Tell the borrower" ]
Read it as English. While you sit here, the case is waiting on the Lending Core. If the payout fails, raise a review. You may leave once the payout is posted, and then go and tell the borrower. Every part of that is a rule the engine enforces, and none of it is code.
An interrupt, mid-journey
Maya Chen’s autopay was returned after she switched banks. The Lending Core raised the delinquency, a collections case opened, and the voice agent worked it: a call, a promise to pay negotiated inside the configured limits, and the callback she asked for. Then a bankruptcy notice arrived while the campaign was live.
Nothing about what follows is special-cased. Every move is one of the mechanisms already described.
Three things in that picture are the pattern rather than the exception.
- Protection does not wait for a review. A bankruptcy or cease signal blocks the exact affected scopes as the signal is recorded, before any review work is accepted. Verification happens afterwards, and the borrower is protected while it does.
- The blocks belong to a case, not to a borrower. Releasing them is a case effect on the case that applied them, so another open case protecting the same person keeps its own.
- Ending a case and unprotecting a borrower are different acts. Only the routes that reach the release step lift the protection. A case that closes some other way leaves it standing, because “this file is finished” and “it is fair to press this person again” are not the same statement.
Changing a flow safely
Flows are business configuration. Editing one does not need a release. It does need a version, and it goes through a governed surface: draft, validate, activate, retire. Drafting and activating are separate permissions on purpose.
Every save runs full schema validation plus catalog-aware checks — every task type, outcome, status and queue named in the definition must resolve against the live catalogs, so the validator never accepts a reference the engine cannot resolve. On top of that, a set of static checks refuses shapes that strand cases.
| The check refuses | Because |
|---|---|
| A route to a step that does not exist | Everything else reasons over the graph, and a graph with an edge to nowhere cannot be reasoned over at all. |
| A step with no way out, or a self-loop with no interrupt | A case that enters can never leave. |
| A step that cannot reach an ending | The same reason, one level up. |
| A rule that reads “unable to complete” on a task | That release is a recovery request, never a step result, so the branch is one nothing can take. |
| Work whose condition already satisfies the step’s own exit | The step becomes free to leave at the instant the work is called for, so the work is withdrawn as fast as it is raised. |
Activating a new version retires the previous one in the same transaction, repoints the case type’s default only where that default followed the same flow, and records a configuration decision. Cases already running stay on the version they started on, and a discarded draft is retired rather than deleted, because configuration history is never hard-deleted.