algorithmModel row does nothing until an operator advances it through four orthogonal lifecycle dimensions. This is the page that explains what those dimensions are, what the safe defaults look like, and the explicit sequence to take a model from creation to scoring real customer requests.
The four lifecycle controls at a glance
A model in
status: "active", registryStatus: "draft" is “operationally live but not a champion” — it can be referenced by name from a decision flow’s score node, but it isn’t the default scorer for its family. A model in status: "draft", registryStatus: "champion" is impossible to construct via the API — the promote endpoint refuses to advance a draft-status model. These two axes are deliberately separate so operators can stage operational rollouts independently of model-evaluation lifecycle decisions.
Out-of-the-box defaults are intentionally inert. Every new model row starts as
status: "draft", registryStatus: "draft", autoLearn: false, learnMode: "none", outcomeWeights: null. There is no automatic “go live” path. This is by design — you should never wake up to find a model you forgot about scoring production traffic.What happens when you POST a model with no overrides
- ❌ Is invisible to
/recommend(filtered out by thestatus: "active"predicate). - ❌ Is not a champion for any registry family (
registryStatus: "draft"). - ❌ Will not retrain on schedule (
autoLearn: false). - ❌ Has no learned state, no metrics, no AUC.
- ✅ Exists in the database and can be inspected via
GET /algorithm-models/{id}.
The four-step path to live champion
To turn the inert row into a model that actually scores production traffic, an operator does four explicit things — and they correspond exactly to the four lifecycle dimensions above.Step 1 — Activate operationally
Setstatus: "active". This makes the model visible to /recommend and to the registry-promote logic. You can do this on creation by passing "status": "active" in the POST body, or via PUT later:
Step 2 — Promote through the registry
Move the model through the lifecycle:draft → shadow → challenger → champion. Each transition is enforced by POST /algorithm-models/{id}/promote and writes an AuditLog row. The “one champion per family” invariant means only one model in each registryFamily can sit at champion at a time — promoting a new one auto-demotes the old.
family is not taken from the promote request body — it’s read from the model’s registryFamily (set when the model was created, defaulting to the model name). The one-champion-per-family invariant auto-demotes the prior champion in the same family to archived in the same transaction.
See Experiments — shadow vs champion/challenger for the full registry lifecycle invariants (auto-rollback guard, one-champion-per-family rule, audit-log row written on every transition).
Promote-endpoint status codes: 404 model not found, 400 invalid transition, 409 rollback-guard breach or missing approval, 200 on success.
Alternative: instead of going through the registry, you can wire the model into a specific decision flow’s score node by its key. The decision-flow engine looks up the score node’s modelKey directly, bypassing the registry-champion resolution. Use this for per-flow specialization (e.g. “this flow’s credit propensity is bayesian-v3 even though the default credit family champion is gbm-v7”).
Step 3 — Enable learning (or accept stasis)
For tabular models, learning is off by default. Without flipping the toggle, your model will keep producing the same scores forever:autoLearn. See Learning cadence for the full per-algorithm cadence table.
Step 4 — Configure outcome weights
outcomeWeights is a JSON map from outcome-type key to a signed numeric weight. The default behavior — when outcomeWeights is null — falls back to +1 for any outcome classified as positive and −1 for any classified as negative. That’s almost always wrong for nuanced workloads.
Reading the lifecycle of an existing model
GET /algorithm-models/{id} returns everything you need to inspect a model’s lifecycle position. Useful field combinations:
What the platform does NOT do automatically
To prevent surprises, the platform deliberately does none of the following:- ❌ Activate models on creation. You must set
status: "active"explicitly. - ❌ Promote models to champion. Even an active model never becomes the default scorer until you
POST /promote. - ❌ Enable auto-learning. Tabular models stay frozen until you flip
autoLearn: true. - ❌ Infer outcome weights. The default
+1/−1mapping is a fallback, not a recommendation. - ❌ Train on creation. Even gradient-boosted with
autoLearn: truewaits for the first cron tick afterlearnScheduleelapses; if you want a one-off immediate retrain, callPOST /algorithm-models/{id}/train.
Bulk operations
Setting up several models at once (e.g. shadow-mode rollout of a model family) is supported but requires the same per-model explicit configuration. The platform does not have a “bulk go-live” endpoint and is unlikely to add one — each model going live should be a deliberate, audited decision. For programmatic setup, the recommended pattern is:shadowScores in decision_traces against the current champion’s scores), continue to challenger and then champion.
Scope hierarchy
A singleAlgorithmModel row is not channel- or direction-bound. Instead, the learned state is kept in ModelAdaptation rows, one per (scope, scopeId) cell. This is how the same Thompson bandit can maintain independent posteriors for “Platinum Card on email” vs “Platinum Card on web” vs “Platinum Card on inbound calls”.
Read order in
/recommend propensity scoring (most-specific to least):
pipeline-runner.ts propensity scoring — channel and category are tighter cells than direction, so they’re consulted first. (The offer+blend fallback at tier 2 uses a slightly different internal order for its shrinkage target — channel → direction → category → global — because it wants the most-specific broader cell.) Each tier has its own evidence threshold before it’s trusted:
The
propensitySource field on the decision trace records which tier fired for each candidate — useful for debugging “why did this offer rank where it did?”.
Storage shape
The(scope, scopeId) row is the unit of adaptation, not the model instance. One model row holds many scoped posteriors, so overlapping cells in the hierarchy don’t duplicate state. This keeps the table compact and easy to reason about — one row to look up, one set of posteriors to compare across scopes.
See also: Learning cadence | Maturity Ramp (BCB-MR) | Uplift Modeling (T/X-learner) | Algorithm Models API | Experiments — shadow vs champion/challenger | Decision Traces — provenance deep-dive