Promotion Policies¶
Promotion is the optional layer on top of decay. Decay handles time-based fade entirely on its own and does not need promotion to function. Promotion does two distinct things:
- Tracks access via
ON ACCESS { ... }—SETmutations write to per-entity access metadata (counters, last-access timestamps, smoothed signals). This is the only mechanism that updates access metadata. - Changes scores conditionally via
WHEN <predicate> APPLY PROFILE '<name>'— when a predicate matches at score time, the named promotion profile'smultiplier/scoreFloor/scoreCapapply to the decayed score.
The two responsibilities are independent:
- A policy with
ON ACCESSand noWHENclauses only tracks. It updates access metadata so aLAST_ACCESSED-anchored decay binding can use it. Scores are not promoted. - A policy with
WHENand noON ACCESSonly promotes. It evaluates predicates against existing properties or pre-existing metadata at score time. - A policy with both does both.
Architecture¶
Promotion has two object kinds. One is an inert parameter package; the other carries a FOR (...) target that selects entities.
| Kind | DDL form | Has target? | What it does on its own |
|---|---|---|---|
| Promotion profile | CREATE PROMOTION PROFILE <name> OPTIONS { ... } | No | Nothing — names a multiplier/scoreFloor/scoreCap set |
| Promotion policy | CREATE PROMOTION POLICY <name> FOR (...) APPLY { ON ACCESS { ... } WHEN ... APPLY PROFILE '...' } | Yes (FOR) | Tracks access, applies promotion profiles when WHEN predicates match, or both |
Creating a Promotion Profile¶
Promotion profiles contain no logic — they are parameter bundles referenced by name inside promotion policy APPLY blocks:
CREATE PROMOTION PROFILE reinforced_tier OPTIONS {
multiplier: 1.5,
scoreFloor: 0.3,
scoreCap: 1.0
}
| Parameter | Default | Description |
|---|---|---|
multiplier | 1.0 | Score multiplier. >1.0 boosts, 1.0 is neutral, <1.0 dampens. A multiplier of 0.5 halves the decayed score; combined with an inverted decay profile (negative halfLifeSeconds) this implements "punish frequent access" semantics where hot nodes are demoted. See Inverted Decay. |
scoreFloor | 0.0 | Minimum score AFTER the multiplier is applied. Different from the decay bundle's scoreFloor — this one acts inside the promoted-curve computation (max(promoFloor, base * multiplier)), the bundle's floor acts as the final clamp on the result. Neither is a visibility cutoff; the suppression check is visibilityThreshold on the decay bundle. |
scoreCap | 1.0 | Maximum score after promotion |
The decay pipeline is
final = max(decayBundle.scoreFloor, min(promoProfile.scoreCap, max(promoProfile.scoreFloor, base * multiplier))), thensuppressed = final < decayBundle.visibilityThreshold. The promotion floor only matters when the matchingWHENpredicate fires; the decay floor matters always. Pick the right floor for the right job.
Creating a Promotion Policy¶
CREATE PROMOTION POLICY <name>
FOR (<target>)
APPLY {
ON ACCESS {
<mutations>
}
WHEN <predicate>
APPLY PROFILE '<profile_name>'
}
Example: Basic Tiered Promotion¶
CREATE PROMOTION POLICY session_record_tiering
FOR (n:SessionRecord)
APPLY {
ON ACCESS {
SET n.accessCount = coalesce(n.accessCount, 0) + 1
SET n.lastAccessedAt = timestamp()
}
WHEN n.accessCount >= 3
APPLY PROFILE 'reinforced_tier'
WHEN n.accessCount >= 5 AND n.sourceAgreement >= 0.95
APPLY PROFILE 'canonical_tier'
}
Example: Negative Multiplier (Access Dampening)¶
A multiplier below 1.0 dampens the score for entities that hit a hot-path threshold. Pairs with an inverted decay profile to invert the recency bias: frequently-accessed entries are demoted, idle entries strengthen over time. See the full walkthrough in Inverted Decay.
CREATE PROMOTION PROFILE access_dampener OPTIONS {
multiplier: 0.5,
scoreFloor: 0.0,
scoreCap: 1.0
}
CREATE PROMOTION POLICY hot_path_dampening
FOR (n:Memory)
APPLY {
ON ACCESS {
SET n.accessCount = coalesce(n.accessCount, 0) + 1
SET n.lastAccessedAt = timestamp()
}
WHEN n.accessCount >= 5
APPLY PROFILE 'access_dampener'
}
Example: Kalman-Filtered Behavioral Signals¶
CREATE PROMOTION POLICY episodic_recall_quality
FOR (n:MemoryEpisode)
APPLY {
ON ACCESS {
SET n.accessCount = coalesce(n.accessCount, 0) + 1
SET n.lastAccessedAt = timestamp()
WITH KALMAN{q: 0.05, r: 50.0} SET n.confidenceScore = $evaluatedConfidence
WITH KALMAN SET n.crossSessionAccessRate =
CASE WHEN n._lastSessionId <> $_session
THEN coalesce(n.crossSessionAccessRate, 0) + 1
ELSE n.crossSessionAccessRate
END
SET n._lastSessionId = $_session
SET n._lastAgentId = $_agent
}
WHEN n.accessCount >= 5 AND n.confidenceScore >= 0.8
APPLY PROFILE 'high_confidence_tier'
WHEN n.accessCount >= 3
APPLY PROFILE 'reinforced_tier'
}
ON ACCESS Semantics¶
ON ACCESS mutations execute when the target entity is read or traversed and passes the suppression gate.
What ON ACCESS does:
- Trigger: every read or traversal of an entity matching the policy's
FORclause fires the block. A query that doesn't touch the entity does not. - Target of writes: the per-entity access metadata store, keyed by entity ID. This store is separate from
n.Properties; the node payload is never mutated byON ACCESS. - Read inside
SET: property reads (e.g.,n.accessCount) resolve from access metadata first, then fall back to stored properties. You can also reference parameters ($x) and use Cypher expressions on the right-hand side. - Timing: mutations are buffered by an access flusher and committed in batches. A read taken immediately after an access may not yet observe the new counter.
- Predicate fairness: suppressed entities (score below
visibilityThreshold) do not accumulate access state, so policies cannot use ON ACCESS to "rescue" hidden items. WHENevaluation: runs at score time against the persisted + buffered access metadata.
What ON ACCESS does not do:
- It does not trigger or "tick" decay. Decay runs on every read regardless of whether any
ON ACCESShas fired. - It does not mutate
n.Properties. The stored payload is unchanged; usepolicy(n)ornornicdb.knowledgepolicy.resolve(...)to inspect access metadata. - It does not return values. The block has no
RETURN; it only mutates.
WITH KALMAN¶
The WITH KALMAN clause applies Kalman filtering to smooth behavioral signals:
| Parameter | Description |
|---|---|
q | Process noise covariance (how much the true value changes between observations) |
r | Measurement noise covariance (how noisy observations are) |
When q and r are omitted, defaults are used. Kalman filtering is appropriate for derived behavioral metrics (access rates, confidence scores) — not for ground-truth values.
Query Context Variables¶
ON ACCESS blocks can access query context variables projected from request headers:
| Request Header | Available As | Purpose |
|---|---|---|
X-Query-Session | $_session | Same-session deduplication |
X-Query-Agent | $_agent | Agent provenance tracking |
Listing and Dropping¶
SHOW PROMOTION POLICIES;
DROP PROMOTION POLICY session_record_tiering;
DROP PROMOTION PROFILE reinforced_tier;
Dropping a promotion profile that is still referenced by an active policy produces a validation error.
See Also¶
- Knowledge-Layer Policies — System overview
- Decay Profiles — Defining decay behavior
- Visibility Suppression — Suppression and deindex behavior
- Ebbinghaus-Roynard Bootstrap — Complete working example with promotion policies and Kalman filtering