Skip to content

Behavior Patterns

Chronicle mines the event history of a store for recurring behavior — combinations of context that keep leading to the same action — and lets you ask what usually happens in a situation. The answer is backed by what actually happened, not by a guess.

The question it answers is: given this user, this day, this time of day, what does this person normally do? An agent, a workflow, or the Workbench can ask it and act on a real answer, or learn that there is no established behavior and say so.

A behavior pattern is a combination of facets that recurred often enough and reliably enough to survive:

PartMeaning
Grouping keyThe scope the behavior belongs to — normally the user
FacetsThe contextual dimensions the pattern constrains, and their values
OccurrencesHow many times it has been observed
ConfidenceHow often it holds when its context is present, 0 to 1
SupportThe share of all observed events it was seen in, 0 to 1
WeightRecency-weighted strength — decays as the behavior goes unseen
First seen / last seenWhen it was first and last observed

A pattern such as { Day: Monday, TimeBucket: Morning, CommandType: ApproveExpenseReport } with a confidence of 0.9 reads as: on Monday mornings, this person approves expense reports nine times out of ten.

Facets are read off an event’s context — never its content — so the same vocabulary applies to every event type in every event store.

FacetWhere it comes from
CommandTypeThe command that produced the event; the event type when nothing above it named itself
InitiatorTypeUser, Agent, System or Unknown
InitiatorIdThe identity that caused the event
OnBehalfOfThe identity it acted for, when it acted for someone else
CausedByCommandThe command one level up the causation chain
CorrelationRootIdThe correlation the event belongs to
AggregateTypeThe event source type the event was appended to
Year, MonthTaken from the event’s occurred timestamp
DayDay of week
TimeBucketEarlyMorning, Morning, Midday, Afternoon, Evening or Night

Every time-derived facet comes from the event’s own occurred timestamp, never from wall-clock time at processing. A backdated append and a replay both land in the bucket the event actually belongs to.

An agent acting on behalf of a person contributes to that person’s behavior, not its own — otherwise one habit would be split across every agent that happened to carry it out. The agent is still recorded as the initiator, so agent-driven and user-driven behavior stay distinguishable.

An event nobody can be named for is not mined at all. Its behavior belongs to no scope, and counting it into a catch-all that every unattributed append pours into produces noise rather than a pattern.

Chronicle does not store anything per event. It keeps a bounded Lossy Counting sketch per scope:

  1. Each event’s facets are expanded into every combination up to MaximumCombinationSize (3 by default). The cap is what keeps the candidate space polynomial rather than exponential.
  2. Each combination is counted in the sketch. Memory is bounded by the Error parameter regardless of how long the stream runs — nothing with a true frequency above the support threshold is ever missed.
  3. A combination that goes unseen decays: its weight is multiplied by DecayFactor for every day since it was last seen, so behavior that stopped happening sinks below the threshold and is pruned instead of competing forever with what a person does now.
  4. Only combinations clearing both MinimumSupport and MinimumConfidence are persisted.

Storage therefore scales with distinct recurring behavior, not with event volume. A store that appends millions of events but sees a few hundred recurring behaviors holds a few hundred rows.

Confidence reads as the rule “in this context, this action follows”: the frequency of the whole combination over the frequency of the same combination without its CommandType facet. A combination that names no action is pure context, and its confidence is its support.

Under Cratis:Chronicle:PatternDetection:

SettingDefaultWhat it does
FacetsCommandType, InitiatorType, CausedByCommand, AggregateType, Day, TimeBucketWhich facets take part in the mined combination
MaximumCombinationSize3The largest number of facets a combination may hold
Error0.001The Lossy Counting error parameter — smaller buys accuracy with memory
MinimumSupport0.01The smallest share of events a combination must hold to survive
MinimumConfidence0.5The smallest confidence a combination must hold to survive
DecayFactor0.99Daily decay applied to a combination that has gone unseen

Year and Month are deliberately absent from Facets: they are kept on a surviving pattern for recency, but combining them multiplies the candidate space by every month the store has been running while splitting one behavior across all of them. Add them when a deployment wants to mine seasonality and can afford the cardinality.

For CommandType and CausedByCommand to be meaningful, something above the event has to name the command. Cratis Arc records a Command causation naming the executing command for the duration of that command, so nested commands produce a real “caused by” chain and sibling commands do not. Anything else appending to Chronicle can do the same by putting a commandType property on its causation.

Without such a link the mined CommandType falls back to the event type, which is still a meaningful action in an event-sourced store — the fact that was recorded is what happened.

Ask what usually happens with the .NET client’s pattern API, or over gRPC through the Patterns service:

  • GetPatterns — given a partial context, the patterns that apply, ranked by specificity and then confidence.
  • GetPatternsForScope — everything a scope has established, unfiltered, for browsing.

Specificity outranks confidence in the ranking. A pattern constraining everything you asked about answers your question; a broader, more confident one answers a question you did not ask.

Nothing clearing the confidence bar returns nothing. An empty answer is a true statement — this scope has no established behavior for this context — and is not padded with the best of a bad set.