Skip to content

Case management (.cmmn)

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 (.ddd machines) 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;
}
}
}
  • case — the declaration. Names are unique per file.
  • roles — case roles a human task’s performer names. A role may bind to an .actor surface actor (role analyst as Analyst): validated authorization metadata, carried on the artifact, with no CMMN semantics.
  • case_file — typed case-file items, using the .mox attribute production (Type[mult]? name;). Multiplicity defaults to 0..1 (the .mox feature 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; optional performer: <role>), service (an execute <alias>.<Service>.<operation>; binding, the use-case realizes rule), decision (an invokes <qualified-name>; binding into a sibling .dmn decision model, below), milestone (a goal; entry criteria only), and stage (grouping; the only kind that nests plan items).
  • Criteria — entry on <part>, <part> when (...); and exit on ...;. An on-part is item.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 bare name naming an event of an imported .evt contract. All on-parts must occur for the criterion to satisfy; the optional when (...) 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), and auto; (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 task marks work a knowledge worker plans into the case at runtime; discretionary milestones are rejected.

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.

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

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.

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 invokes clause may carry version compatible or version "X.Y.Z" — the same pin grammar and resolution rules the .ddd decision references use (pherolang#154; see the .dmn surface’s Versioning section): an exact pin resolves to the same-name service at that version, compatible to 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.LoanAssessmentService and the bare LoanAssessmentService are the same service) union-wide against the provided .dmn models’ decision services. An empty decisions bucket lowers the clause unvalidated (the plain-compile_ddd_str records-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/outputs name 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 (Verdict above 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 the phero_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.

Terminal window
pherolang fmt file.cmmn # canonical layout (also --check)
pherolang check tests/conformance/ # directory batch: every surface, then the union integrity pass per application
pherolang ir file.cmmn -o case.json # the CmmnModel artifact
pherolang artifact check case.json # classifies + lint-checks the artifact

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

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.