Case management (.cmmn)
Overview
Section titled “Overview”A .cmmn file declares a case: the adaptive, event-and-condition-driven
work coordinated around a domain — the CMMN 1.1 (OMG formal/2016-06-01)
notion of case management, in the Phero Lang idiom. A case is not a
workflow and not a state machine: its plan declares the work that is
available (tasks, service tasks, milestones, grouped into stages) and
the criteria under which each item activates, without prescribing a
sequence. Sibling tasks are concurrent by default; ordering exists only
where a criterion imposes it.
The division of responsibility this surface codifies:
- The domain (
.mox) owns the data and its invariants. - The domain lifecycle (
.dddmachines) owns an aggregate’s valid state transitions. - The case owns the work: who does what, when, and why.
- The engine executes — Phero Lang authors and validates, the cmmn-rs engine runs compiled definitions (see the backend).
A lending application is the canonical shape: VerifyBorrower and
CollectFinancials can run concurrently; CreditAssessment activates only
once both complete; a credit-committee milestone marks the goal. The
application’s aggregate state stays in the domain; the case coordinates the
work around it. The domain owns its state; the case owns its work.
The fence below is the canonical fixture’s shape
(tests/conformance/cmmn/lending.cmmn), its imports repointed so it
compiles from this document (the docs fences are compiled by CI):
import "../tests/conformance/cmmn/lending.mox"import "../tests/conformance/cmmn/lending.evt"import "../tests/conformance/cmmn/lending.actor"import "../tests/conformance/cmmn/lending.ddd" as lending
case CommercialLoan {
roles { role relationship_manager as RelationshipManager; role credit_analyst as CreditAnalyst; }
case_file { /// The borrower under assessment. Borrower[0..1] borrower; /// Financial statements received during assessment. FinancialStatements[] financials; }
stage Intake { task VerifyBorrower { performer: relationship_manager; } milestone ApplicationReady { entry on VerifyBorrower.complete; } }
stage Assessment { entry on ApplicationReady.occur; task CreditAssessment { performer: credit_analyst; entry on ApplicationReady.occur; exit on AssessmentComplete.occur; } service ScoreApplication { execute lending.CreditAssessmentService.assess; entry on financials.create, financials.update when (financials.size() > 0); required; } milestone AssessmentComplete { entry on CreditAssessment.complete, ScoreApplication.complete; } }}Anatomy
Section titled “Anatomy”case— the declaration. Names are unique per file.roles— case roles a human task’sperformernames. A role may bind to an.actorsurface actor (role analyst as Analyst): validated authorization metadata, carried on the artifact, with no CMMN semantics.case_file— typed case-file items, using the.moxattribute production (Type[mult]? name;). Multiplicity defaults to0..1(the.moxfeature convention);[1..1],[0..*]/[], and[1..*]are the other engine-supported shapes. Plan items and case-file items share one namespace per case, so criterion references stay unambiguous.- Plan items —
task(human work; optionalperformer: <role>),service(anexecute <alias>.<Service>.<operation>;binding, the use-caserealizesrule),decision(aninvokes <qualified-name>;binding into a sibling.dmndecision model, below),milestone(a goal; entry criteria only), andstage(grouping; the only kind that nests plan items). - Criteria —
entry on <part>, <part> when (...);andexit on ...;. An on-part isitem.word— a plan item standard event (create,enable,start,complete,terminate,fail,occur) or a case-file transition (create,update,delete,addChild,removeChild) — or a barenamenaming an event of an imported.evtcontract. All on-parts must occur for the criterion to satisfy; the optionalwhen (...)if-part is a boolean phero-expr condition over the case file, type-checked at compile time (below). Milestones carry no exit criteria. - Flags —
required;(the containing stage cannot complete without the item),repeats;(repetition on completion), andauto;(auto-start — an explicit FALSE manual-activation rule; the CMMN default is manual activation, so entry criteria enable a task and a human starts it).discretionary taskmarks work a knowledge worker plans into the case at runtime; discretionary milestones are rejected.
Artifact wire format
Section titled “Artifact wire format”pherolang ir file.cmmn emits the standalone
phero_ir::cmmn::CmmnModel artifact — the
versioned wire format in the Phero Lang artifact family
(CMMN_MODEL_FORMAT_VERSION, camelCase keys, adjacent tagging, an empty
artifact serializing as exactly {"formatVersion":2}). The wire carries
names as written and when conditions verbatim (the wire rule 9
convention); resolution and expression semantics are driver concerns, never
wire concerns. The plan is a flat graph: stages reference children by
name, and a case’s root items are the ones no stage lists.
Grammar
Section titled “Grammar”The normative EBNF lives once in the parser module docs
(crates/phero-syntax/src/parser/mod.rs,
section “.cmmn case management sources”); pherolang fmt canonicalizes to
the layout documented on format_cmmn.
Every case word is a contextual identifier (escapable with ^);
import, as, on, and when are real lexer keywords. The transition
vocabularies are deliberately not syntax — both segments of an on-part
are plain names and the driver validates the tail against its closed
tables (the .deploy vocabulary discipline).
Resolution & validation
Section titled “Resolution & validation”compile_cmmn_str (normative contract on
crates/phero-driver/src/lib.rs) resolves a
.cmmn file against five import buckets: raw .mox domains, plus
pre-compiled EventModel, ActorModel, DddModel, and DmnModel pairs
the host provides. Imports match by exact path or lexical resolution relative
to the case file (the .deploy rule); only design imports take an alias,
because aliases exist for execute.
The numbered rules (quoted here in brief, specified in full on the entry
point): unprovided imports; case-name uniqueness; role uniqueness and
.actor binding resolution; case-file item typing through the domain
union (unknown type, ambiguous type); the shared plan-item/case-file
namespace; performer placement and role resolution; execute placement,
necessity, and design resolution; discretionary placement; criterion
resolution (milestone exit criteria, on-part sources, transition
vocabularies, event resolution); when typing; and decision-task
placement, service resolution, and case-file binding
(below).
when conditions type-check against a synthetic case universe (the
.deploy settings-universe pattern): the case file as a class whose
features are the case-file items (typed by the imported domains), each plan
item as a class of boolean transition predicates, and the case itself as
the root class. case_file.financials.size() > 0 and
VerifyBorrower.complete are plain feature navigation from self; the
condition must be boolean, and the expression rules R1–R9 of
docs/EXPRESSIONS.md apply and are cited in errors. Every
resolved decision task extends the universe with its service’s output
decisions — below.
Decision tasks
Section titled “Decision tasks”A decision plan item invokes a decision service declared in a
sibling .dmn decision model — the decision dimension
(docs/DMN.md) as case work:
import "../tests/conformance/cmmn/decision.mox"import "../tests/conformance/cmmn/decision.dmn"
case Underwriting { case_file { /// The application under assessment. Application[1..1] application; /// The assessment once the decision task has run. Assessment[0..1] assessment; } stage Assessment { entry on application.create; decision assess { invokes Underwriting.LoanAssessmentService; inputs { application } outputs { assessment } entry on application.create when (Verdict); required; } milestone Assessed { entry on assess.complete; } }}The body carries exactly one invokes <qualified-name>; clause (a
duplicate is a parse error, the execute-clause rule) plus optional
inputs { <name>... } and outputs { <name>... } brace
name-lists — the .dmn service name-list shape, whitespace-separated
names. Canonical member order is invokes, inputs, outputs, then the
criteria and flags; decision tasks take no performer (the
only-a-human-task-carries-performer rule extends, as do the stray-clause
diagnostics for inputs/outputs on a non-decision item).
- Version pins. The
invokesclause may carryversion compatibleorversion "X.Y.Z"— the same pin grammar and resolution rules the.ddddecision references use (pherolang#154; see the.dmnsurface’s Versioning section): an exact pin resolves to the same-name service at that version,compatibleto the highest declared version in the highest major present, and an unsatisfiable pin names the service, the pin, and the versions available. - Resolution. The qualified name resolves by its last segment (the
rule-17 convention:
Underwriting.LoanAssessmentServiceand the bareLoanAssessmentServiceare the same service) union-wide against the provided.dmnmodels’ decision services. An empty decisions bucket lowers the clause unvalidated (the plain-compile_ddd_strrecords-names-unvalidated precedent); a non-empty bucket with an unknown service errors naming the task and the service, with a did-you-mean help line over the declared set. The artifact lowers the name as authored. - Case-file binding. Every
inputs/outputsname must be a declared case-file item of the same case (the execute-binding existence bar; a plan-item name is not enough). Unknown names error with a did-you-mean help over the case’s case-file items. - Universe. Every resolved service’s output decisions join the
synthetic case universe, so guards can reference them: a single-output
decision inserts an entry named by the decision typed with its output
type (
Verdictabove is a boolean guard); a multi-output decision inserts a class named by the decision whose fields are its named outputs (LoanAssessment.outcome). Unresolved tasks contribute nothing. The universe is assembled on thephero_expr::DomainTypes(insert_class) — never a fake model.
Documented deferrals. There is no type-agreement check between the
bound case-file items and the service’s declared input/output types — the
binding is by name; the runtime executor owns the value mapping. The
engine CaseDefinition has no slot for task inputs/outputs either: the
backend lowers a decision task exactly like a service task
(taskType: "Process", processRef = the authored invokes name —
the key a decision-task executor registry resolves) and leaves the
input/output mapping to that registry; it invents no engine fields.
Tooling
Section titled “Tooling”pherolang fmt file.cmmn # canonical layout (also --check)pherolang check tests/conformance/ # directory batch: every surface, then the union integrity pass per applicationpherolang ir file.cmmn -o case.json # the CmmnModel artifactpherolang artifact check case.json # classifies + lint-checks the artifactThe cmmn-rs backend (phero-backend-cmmn) lowers a compiled CmmnModel
plus its domain model into the engine’s CaseDefinition JSON — the case
file items (multiplicity vocabulary, domain-class features as properties),
a synthetic root stage, generated sentries, User tasks with
requiredRole, service tasks and decision tasks as Process compositions
keyed by their binding (the execute reference and the authored
invokes name respectively — the engine’s executor-seam processRef), and
discretionary tasks as planning tables. when conditions translate
phero-expr → FEEL within a documented subset (&&→and, size()→count,
case_file.x→x, if/then/else, …); everything outside the subset is a
named generate-time error (the Cedar-backend convention), as are the
two engine gaps the surface accepts today: bare .evt event on-parts (the
engine’s OnPart is plan-item/case-file transitions only) and plan-item
predicates inside when conditions (the engine evaluates if-parts against
the case-file scope alone).
Known follow-up: a dev-dependency on cmmn-rs (the cedar-policy
precedent) to validate the emitted definitions with the real
deserialize_case_definition in CI.
Conformance
Section titled “Conformance”The canonical fixture set lives in tests/conformance/cmmn/:
lending.cmmn (the case above, over lending.mox/lending.evt/
lending.actor/lending.ddd) compiles clean, byte-matches the
lending.cmmn.json artifact golden, and round-trips through from_json
(crates/phero-driver/tests/cmmn_golden.rs); the backend lowers the same
artifact to the engine’s CaseDefinition JSON, byte-pinned at
lending.case.json (crates/phero-backend-cmmn/tests/generate.rs).
decision.cmmn (the decision-task fixture, over decision.mox and
decision.dmn) pins the decision kind end to end — binding, resolution,
universe — with its own decision.cmmn.json artifact golden and the
exact-diagnostic negative tests in the same suite. The examples/ side
ships support.cmmn — the full-stack example whose imports span all five
buckets, whose execute binding names a real support.ddd service
operation, and whose decision task invokes support.dmn’s
RefundDecisions.AssessRefund service — byte-pinned under
examples/artifacts/ (examples_artifacts.rs, plus the engine-definition
golden in phero-backend-cmmn’s examples test). The CI fmt gate covers
tests/conformance/cmmn/*.cmmn and examples/*/*.cmmn. Regenerate goldens
with PHERO_UPDATE_FIXTURES=1 cargo test --workspace.