The Model Garden & Atelier
Author a custom Bayesian model once, prove it against an oracle compatibility suite, publish it to your organization — and let every analyst fit, interrogate, and report on it through the agent without ever touching the kernel.
Most bespoke marketing-science work dies in a notebook. A methodologist builds a clever structural model for a particular client question, it produces one result deck, and the code is never reused — there is no safe way to put a hand-rolled PyMC model into a shared application, and no guarantee it behaves like a first-class model once it gets there. The Model Garden is the answer to that: a governed, versioned registry of custom Bayesian models, and the Atelier is the in-app IDE where you write, test, document, and publish them. A published model becomes a named asset that any project in your org can re-fit on its own data — through the same agent, the same reports, the same estimands as the built-in MMM.
📊 Analysts & consultants
You never write Python. Ask the agent which models exist, pick one, point it at your data, and fit. You get the model's headline estimands, ROI, decomposition, diagnostics, and a report — exactly as you would for a built-in model. Jump to Using a model with the agent.
🛠️ Authors & methodologists
Subclass one base class, override one or two hooks, declare your parameters and estimands, and let the compatibility suite prove the model is well-behaved before it ships. Jump to The authoring recipe.
💡 Where this fits
The Atelier is one surface of the platform tour in the Platform Overview. For the modelling concepts underneath a garden model — priors, adstock, saturation, identification — see the Modeling Guide and Identification Assumptions. This page is about the ecosystem: how a custom model is made, governed, shared, and used.
Why a Model Garden
Three problems sit between a good idea in a notebook and a model an organization can rely on:
- Deployment. Putting new model code into a running application normally means a code review, a release, and a redeploy of the runtime. That is far too much friction for a one-question structural model.
- Trust. A hand-rolled model might sample badly, mis-scale its outputs, or silently break the read-side metrics (ROI, decomposition) that downstream tools expect. Without a quality bar, nobody can safely reuse it.
- Reuse. Even a good model is stranded if there's no catalog, no versioning, and no way for a teammate on another project to find and run it.
The Model Garden removes all three. A model is loaded by reference, not by
redeploying code; it must pass a compatibility suite before it can be marked
tested; and it lives in an org-scoped, versioned registry that the agent can
search. The mechanism that makes the first point work is a small piece of provenance called
the garden_ref:
# A garden_ref — embedded in a session spec and in saved-model metadata.
# It is just enough to reconstruct the exact authored class on load.
garden_ref = {
"name": "awareness_structural_mmm",
"version": 3,
"source_path": "…/garden/awareness_structural_mmm/v3/model.py",
"class_name": "AwarenessStructuralMMM",
"contract_version": "1.0",
}
When the agent fits a model, it reads spec["garden_ref"], the kernel imports the
exact authored class from source_path, and the fit proceeds. The application
runtime never needs the model's code baked in — authors ship science, not
infrastructure. If the source is ever missing, the loader degrades gracefully to the
base BayesianMMM rather than failing the session.
The business value of a growing library
Every model that clears the bar and gets published is a permanent, governed, documented asset — and because the agent can discover and run it, the value of authoring one accrues to every future analyst session. The library compounds.
Ship without a redeploy
Loading by garden_ref means the runtime never needs new code to use a
new model. Registering and publishing is the release — turnaround is minutes,
not a deploy cycle.
One author, every project
Models are org-scoped, so a methodologist's awareness model, CFA, or
latent-class model is reusable across every project in the tenant with a single
load_garden_model call.
Reproducible & auditable
Monotonic per-(org, name) versioning, immutable published versions,
an append-only status history, and serialized garden_ref provenance mean
a saved result can always be traced back to the exact class that produced it.
A real quality bar
The compatibility suite fits each candidate on synthetic worlds with known ground
truth and gates draft → tested automatically. Pathological priors,
geometry, or scaling are caught before a model touches client data.
Host-safe contributions
Registration validates source with AST only — it is never executed on the host. Untrusted author code runs solely in the sandboxed kernel during test and fit, so an org can accept models from many authors without trusting their code on the API process.
Beyond marketing mix
Non-MMM families — factor analysis, latent-class segmentation, structural awareness models — register, test, fit, expose estimands, serialize, and report through the same pipeline. The catalog grows in scope without a parallel system.
The compounding loop
Author → prove → publish → every analyst can use it in one chat turn. Each model raises the leverage of every future session, and the Atelier's copilot lowers the cost of contributing so more methodologists can add to the library safely.
Using a registered model with the agent
For an analyst, a custom model is no different from a built-in one. You talk to the agent in plain language; it calls the right tools; the model fits and reports. Here is the whole flow.
Three published models match. awareness_structural_mmm (v3, published, compatibility 0.94) models a latent goodwill stock with a survey-count or index KPI…
→ fit_mmm_model(…)
Fitted. The model's declared estimands are ready — awareness lift and contribution ROI.
Awareness lift +6.2pp (89% HDI 3.1–9.4), contribution ROI 2.3× … report attached.
What is actually happening under those tool calls:
- Discover —
list_garden_modelsreturns the org's catalog (name, version, status, compatibility score, docs). - Load —
load_garden_modelstages the model into the session: it copies the source into the workspace, setsspec["garden_ref"], and applies the model'srecommended_fitto the inference settings. - Fit —
fit_mmm_modelreads the spec and dispatches to the kernel, where the build step resolves the garden class fromgarden_ref, validates anymodel_paramsagainst the model's config schema, wires the declared likelihood and estimands, and fits the bespoke subclass. - Interrogate —
get_estimandsrealizes the model's declared estimands post-fit (mean, HDI, units, probability of being positive/profitable). Standard read-ops — ROI, decomposition, adstock, saturation, diagnostics — work on a garden model with no special-casing. - Report — reporting is family-aware: an MMM model gets the full marketing report; a non-MMM family gets its family-specific section (factor loadings, class profiles) plus its declared estimands, chosen automatically by the model's kind.
Agent tools at a glance
| Tool | Role | What it does |
|---|---|---|
list_garden_models | any | Browse the org's catalog (name, version, status, compatibility, docs). |
load_garden_model | any | Stage a model into the session spec via garden_ref + recommended fit. |
fit_mmm_model | any | Fit the staged model (resolves the garden class kernel-side). |
get_estimands | any | Realize the model's declared estimands (mean + HDI + units). |
register_garden_model | analyst+ | Register new source as a draft (AST-validated, not executed). |
test_garden_model | analyst+ | Run the compatibility suite; auto-promote draft → tested on a clean pass. |
publish_garden_model | admin/owner | Promote tested → published so every project can load it. |
What the analyst never has to do
Write the model, install anything, or know its internals. The model's assumptions live in its docstring and docs page; its measurable outputs live in its declared estimands; the agent handles the rest.
The Atelier — an IDE for models
The Atelier is a Jupyter-like workspace for authoring, testing, and publishing models, opened from the Model Garden page. The layout is a three-column grid:
- Left (collapsible) — the registry. Every model your org has, with a version picker and lifecycle status chips.
- Center — three tabs. Code (a Monaco editor with framework completions and a copilot side-panel), Docs (a markdown editor with live preview), and Notebook (Jupyter-like cells you run against real data).
- Right (collapsible) — two tabs. Compatibility (the tier-by-tier test report and fit-quality score) and About (class name, contract version, docs, and status history).
The authoring loop in the UI
- New model. The Code editor seeds a
CustomMMMstarter; name the model. - Write & check. Edit the subclass; use Format (Ruff) and Validate (a static AST lint that surfaces Problems) from the toolbar — alongside Wrap, Minimap, font zoom, fullscreen, and the Copilot toggle.
- Ask the copilot. The inline Bayesian-modelling assistant streams answers; Apply to editor merges a suggested code block straight into the buffer.
- Document. Write the model's markdown docs in the Docs tab with live preview.
- Register. Register draft stores the source (AST-validated, not
executed) as version 1, status
draft. - Demo on real data. In the Notebook tab, upload a CSV (it auto-binds as
df), add code cells, and run them against the live editor buffer to sanity-check the model. An errored cell offers Diagnose with copilot, which rewrites the failing cell — or the model source — for you. - Prove. Run compatibility test streams the tier report; on a
clean blocking pass the model auto-promotes to
testedand a Publish button appears. - Publish. An admin/owner clicks Publish; the version becomes immutable and loadable by every project in the org.
The lifecycle is always visible as status chips:
Draft
Registered. Source stored, AST-validated. Editable.
auto · compatibility passTested
All blocking tiers pass on synthetic worlds. Editable docs.
human · publish gatePublished
Org-wide, immutable, loadable by every project.
Deprecated
Retired but preserved in history (never deleted).
💡 The Notebook runs your live edits
The Notebook executes cells against the source currently in the Code editor, not against a saved snapshot. That makes the loop tight: change a prior, re-run the cell, see the new posterior — all before you register a draft. Notebook state is persisted per model + version, so a model carries its own demo.
Building a model
For authors, the contract is deliberately small. You subclass one base class, override the graph, and declare what the model measures. Everything else — fitting, serialization, estimand evaluation, reporting — is inherited.
The minimal skeleton
Subclass CustomMMM (a thin, documented marker over BayesianMMM that
stamps the contract version). The one method you almost always override is
_build_model; keep the standard constructor signature so the agent can swap your
class in for the built-in one.
from mmm_framework.garden import CustomMMM
class MyStructuralMMM(CustomMMM):
"""One-paragraph statement of WHAT this model assumes and WHY.
State the identification strategy here: the priors, any sign
constraints, and the fixed structural pattern. A consumer who fits
this model on their data is inheriting these assumptions.
"""
def _build_model(self):
# Your PyMC graph. Register every reported quantity as a named
# Deterministic, in ORIGINAL KPI scale. For MMM models, alias
# coefficients / carryover / saturation to the PARAM_PREFIXES
# (beta_, adstock_alpha_, sat_half_,
# sat_slope_) so the reporting helpers can find them.
...
Before you register, you can prove the structure statically — no PyMC, no fit — and then run the full suite. These are the real, importable entry points an author uses locally:
from mmm_framework.garden import CustomMMM, validate_class, run_compatibility_check
# Static, PyMC-free structural check (empty list == OK):
problems = validate_class(MyStructuralMMM)
# Full nine-tier suite — fits on synthetic worlds, never raises:
report = run_compatibility_check(MyStructuralMMM)
Config & likelihood
Two axes let a model carry its own settings and its own observation model without forking the framework.
Settable parameters. Instead of magic numbers, declare a class attribute
CONFIG_SCHEMA — a pydantic.BaseModel of defaulted, constrained,
described fields. The agent validates the caller's model_params against it,
passes them to your constructor, and reads them as self.model_params; the
serializer round-trips them; and the schema is exported as JSON Schema to drive a UI form.
Observation model. A model declares its likelihood family and reads it in
_build_model. The built-in additive dispatch handles Gaussian families
(NORMAL, STUDENT_T) byte-identically; a non-Gaussian KPI (a survey
count, a bounded rate) owns its observation node — the additive base raises
NotImplementedError for non-Gaussian on purpose, so count/bounded models write
their own.
from mmm_framework.config import LikelihoodConfig
from mmm_framework.config.enums import LikelihoodFamily, LinkFunction
Available families: NORMAL, STUDENT_T, LOGNORMAL,
BINOMIAL, BETA_BINOMIAL, POISSON,
NEGATIVE_BINOMIAL, and BETA.
Declaring estimands
A model's DEFAULT_ESTIMANDS are the named quantities it surfaces through the
estimand engine — what the agent reports and what the report renders. They are the model's
measurement contract: document what the model measures with declared estimands, not
just prose.
For MMM channel quantities you reference the built-ins by name — e.g.
"contribution_roi", "marginal_roas", "contribution".
For latent quantities (a fit index, a named scalar, a class size) you build an
Estimand with a public registry helper:
from mmm_framework.estimands.registry import latent_scalar, fit_index, factor_loading
Each is gated by a HAS_LATENT:<var> capability, so it cleanly returns an
unsupported status (never an exception) on a model that doesn't carry the variable.
If your estimands scale with config — say one class-size estimand per class — override
_default_estimands() to build them dynamically.
⚠️ Posterior names and scale are conventions, not suggestions
The MMM reporting helpers key off the PARAM_PREFIXES
(beta_<channel>, adstock_alpha_<channel>,
sat_half_<channel>, sat_slope_<channel>). Use a
different name — coef_<channel> — and the ROI/decomposition helpers
silently return empty. And per-observation deterministics must be in
original KPI scale: standardized-scale predictions fail the
scaling compatibility tier.
Non-MMM families
The same pipeline supports genuinely non-marketing-mix Bayesian families. A model opts out of
the MMM-only gates by declaring a non-"mmm" kind:
class BayesianCFA(CustomMMM):
__garden_model_kind__ = "cfa" # opt out of channel / beta / scaling gates
def _prepare_data(self):
# Assemble your observed indicators; then set the model-agnostic
# attributes the inherited fit + serializer read:
self.channel_names = [] # empty list (not None) signals "no channels"
# … n_obs, y_mean=0, y_std=1, _media_raw_max={}, time_idx, etc.
...
def _build_model(self):
... # your latent-variable graph
With channel_names = [] the contract recognizes the family and skips the
channel, beta_<channel>, scaling, and ops-smoke checks; the core
requirements (a real fit, a posterior with chain/draw dims, your
own declared estimands) still apply. The estimand engine auto-exposes every posterior
variable as a HAS_LATENT:<var> capability, so fit_index(…),
latent_scalar(…), and factor_loading(…) just work, and the
family-agnostic report renders your summary table plus your declared estimands.
Worked examples to fork
Three complete, tested models ship in the repository under
examples/garden_models/. Each is a good starting point:
mmm
awareness_structural_mmm.py
A latent goodwill-stock model with a CONFIG_SCHEMA and a pluggable
likelihood: a Normal index KPI or a Binomial survey-count KPI. Declares
awareness_lift + contribution_roi, and demonstrates a latent
contrast (goodwill under media-on vs a channel off).
cfa
bayesian_cfa.py
A confirmatory factor analysis. A marginal multivariate-normal likelihood
\( y_i \sim \mathcal{N}(0,\ \Lambda\Lambda^\top + \Psi) \) with positive loadings on a
fixed simple structure; per-draw srmr/cov_fit fit-index
estimands; a factor_loadings_summary() table.
latent_class
bayesian_lca.py
A latent-class (mixture) model over binary indicators. Discrete labels integrated out
with a logsumexp log-mixture; an ordered logit pins class order;
dynamic per-class class_size_k estimands and a
class_profile_summary() table.
The deep specifications live in technical-docs/:
custom-model-config.md (config + likelihood),
estimands.md (the estimand subsystem), and
non-mmm-families.md (the CFA/LCA contract).
The compatibility contract
A garden model has to behave like a first-class model, so it is checked at three points and then exercised end-to-end on synthetic worlds with known ground truth.
Three validation stages
| Stage | When | Checks |
|---|---|---|
validate_class | Static (host-side, PyMC-free) | Class structure, required methods, the fit signature, and either a BayesianMMM subclass or an explicit non-MMM kind. Runs before the registry write. |
validate_instance | Post-init | Required attributes are present and sane; for MMM models, channel_names is a non-empty ordered list and _media_raw_max covers every channel. |
validate_fitted | Post-fit | The trace is set with a posterior group and chain/draw dims; MMM models carry beta_<channel> parameters. |
The one truly required method is fit() — it must run and leave a
fitted trace (an ArviZ InferenceData with a posterior group). The
recommended methods (predict,
sample_channel_contributions, compute_component_decomposition) each
unlock read-ops; their absence must degrade gracefully, not raise. Required attributes are
split into a model-agnostic base set and an MMM-only set, so non-MMM families carry only the
base.
The nine tiers
run_compatibility_check runs nine tiers and never raises — each
tier reports passed / skipped / detail independently. Seven are blocking:
a clean pass on all of them is what auto-promotes draft → tested.
| Tier | Blocking | What it proves | Non-MMM |
|---|---|---|---|
static | yes | validate_class passes (structure, methods, fit signature). | runs |
build | yes | The class instantiates on a synthetic world via the agent's build step. | runs |
fit | yes | A MAP fit completes and sets the trace. | runs |
instance | yes | validate_instance passes (attributes, channel/media coverage). | runs |
trace | yes | validate_fitted passes (posterior structure). | runs |
scaling | yes | Original-scale predictions are finite and within 0.01–100× of the KPI mean. | skipped |
ops_smoke | yes | Five oracle read-ops (ROI, decomposition, diagnostics, adstock, saturation) smoke-test cleanly. | skipped |
carryover | optional | On a 2-geo panel, a spend spike in one geo does not bleed to the other (per-cell adstock). | optional |
accuracy | optional | Recovered ROI agrees directionally with the synthetic answer key. | skipped |
The gate that decides which checks apply is is_mmm_model(): a model is treated
as an MMM unless it explicitly declares a non-"mmm"
__garden_model_kind__. Promotion to tested is automatic on a clean
blocking pass; promotion to published is always a deliberate human decision.
Test the spec you intend to ship
A model can pass on one configuration and fail on another — a non-Gaussian likelihood builds a different graph than a Normal one. Treat the compatibility suite as the merge gate and run it against the exact spec you mean to publish.
Documenting a model — best practices
A garden model is a scientific claim that other people will inherit. Documentation is not an afterthought; several surfaces carry it, and the agent and reports read them directly.
- The class docstring states the why and the identification strategy — priors, sign constraints, fixed structural patterns. A consumer fitting your model is accepting these assumptions, so name them.
CONFIG_SCHEMAfield metadata (defaults, constraints, descriptions) becomes validated params, a serialized round-trip, and the UI's params form. Put every tunable here rather than in magic numbers.DEFAULT_ESTIMANDSis the machine-readable statement of what the model measures — it is what the agent surfaces and the report renders.- The markdown docs page (the Docs tab) is the human narrative. It is editable in place on a draft or tested version, and immutable once published — to change a published model's docs or source, register a new version.
- A summary method for any matrix-valued quantity (e.g.
factor_loadings_summary(),class_profile_summary()) gives the family-agnostic report a table to render.
Author's checklist
- State the identification strategy in the docstring.
- Put every tunable in
CONFIG_SCHEMAwith defaults + constraints. - Name posterior params on-convention (
beta_<channel>, …) and emit per-observation deterministics in original KPI scale. - For non-MMM families, set
channel_names = []and declare family estimands via the registry helpers. - Declare what the model measures with
DEFAULT_ESTIMANDS. - Run the compatibility suite on the spec you intend to ship — all blocking tiers green.
- Publish deliberately; revise by registering a new version; deprecate rather than delete to preserve history.
Governance & security
The registry is built so an organization can accept models from many authors without trusting their code on the application process, and so published results stay reproducible.
- Host-safe registration. Registering a model AST-validates the source (it detects the class name and the declared kind) and never executes it on the host. Untrusted author code runs only kernel-side — a sandboxed subprocess or container in the hosted profile — during test and fit.
- Roles. Registering, linting, and testing are analyst+; publishing and deleting are admin/owner. Publishing is the one human gate that matters.
- Org scoping. Models are shared across all projects in an org (the sharing boundary is the org, not the project), so a published model is immediately reusable everywhere in the tenant.
- Immutability & audit. Versions are monotonic per (org, name);
a published version is frozen; a status history is appended (never overwritten); and a
saved model carries its
garden_ref, so any result traces back to the exact class. To change a published model, register a new version. Retire with deprecate, which preserves history — deletion is only for drafts and deprecated versions.
The REST governance surface
The Atelier UI and the agent tools share one governed REST surface. The blocks below are illustrative routes, not a runnable client.
| Method & path | Role | Purpose |
|---|---|---|
POST /model-garden | analyst+ | Register source as a draft (AST-validated, not executed). |
POST /model-garden/lint | analyst+ | Static Problems check for the editor (syntax, class, conventions). |
POST /model-garden/format | analyst+ | Ruff/Black format the editor buffer. |
POST /model-garden/copilot | analyst+ | Streaming (SSE) Bayesian-modelling copilot; stateless. |
GET /model-garden | any | List the org's models (latest per name by default). |
GET /model-garden/{name}/versions | any | Every version of one model, newest first. |
GET /model-garden/{name}/{version} | any | One version, with manifest + compatibility report. |
GET /model-garden/{name}/{version}/source | any | Stored source text for the Atelier editor. |
PATCH /model-garden/{name}/{version} | analyst+ | Edit docs in place (409 if published). |
POST /model-garden/{name}/{version}/test | analyst+ | Start a non-blocking compatibility job; auto-promotes on pass. |
GET .../test/{job_id} | analyst+ | Poll the test job for the tier report + score. |
POST /model-garden/{name}/{version}/promote | admin/owner | Human publish gate (tested → published). |
DELETE /model-garden/{name}/{version} | admin/owner | Delete a draft or deprecated version only. |
A parallel set of notebook routes backs the Atelier's Notebook tab — get/put the notebook
doc, upload a dataset (auto-binds as df), and run cells as non-blocking jobs.
The manifest
Each registered version carries a manifest of advisory, additive discovery metadata. It powers the UI and the catalog; the authoritative source of a model's capabilities is always the runtime model itself, not the manifest.
| Field | Meaning |
|---|---|
contract_version | The garden contract the model was written against. |
class_name | The authored class to reconstruct (AST-detected). |
model_kind | The family (mmm, cfa, latent_class, …) for discovery/filtering. |
config_schema | JSON Schema of CONFIG_SCHEMA for the dynamic params form. |
default_estimands | What the model claims to measure. |
recommended_fit | Inference settings load_garden_model applies by default. |
capabilities · tags · dataset_schema | Discovery hints, search tags, and the expected data shape. |
demo_notebook | Optional curated cells, seeded the first time the model's Notebook is opened. |
The growing ecosystem
Put the pieces together and you get a flywheel. A methodologist authors a model in the Atelier with copilot assistance; the compatibility suite proves it is well-behaved; an admin publishes it; and from that moment it is one chat turn away from being fitted, interrogated, and reported on by any analyst on any project in the org — on their own data, with full provenance.
Because non-MMM families ride the same contract, the catalog is not limited to marketing mix: factor models, segmentation models, and structural awareness models extend it without a second system. Every published model raises the leverage of every future session, and the lower the authoring cost — copilot, notebook diagnosis, a small contract — the more experts can contribute. That is the point of the garden: a library that gets more valuable the more it grows.
Get started
The fastest path from curiosity to a published model:
- Read the three example models under
examples/garden_models/—awareness_structural_mmm.py,bayesian_cfa.py,bayesian_lca.py— and fork the closest one. - Open the Atelier, paste it into the Code tab, and tweak the structure with the copilot.
- Upload a dataset in the Notebook tab and sanity-check the fit on real data.
- Run the compatibility test; fix anything that isn't green.
- Write the docs, register, and — once tested — ask an admin to publish.
💡 See it pre-populated
A seeder ships the three example models already published and a demo project that shows the agent using each one in the Oracle (discover → load → fit → estimands), with the real fitted numbers:
uv run python scripts/seed_atelier_demo.py # real MAP fits (~10s)
uv run python scripts/seed_atelier_demo.py --fast # no MCMC (seconds)
Then open the Atelier to browse the published registry, and select “Demo: Atelier Custom Models” in the project switcher to read the seeded Oracle sessions.
Bring your model in from the cold
See where the Atelier lives in the platform, then dig into the modelling concepts behind a good garden model.