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.

If you consume models

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

If you build models

🛠️ 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:

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.

You What custom models do we have for awareness studies?
Agent → list_garden_models()
Three published models match. awareness_structural_mmm (v3, published, compatibility 0.94) models a latent goodwill stock with a survey-count or index KPI…
You Use that one and fit it on the brand-tracker data I uploaded.
Agent → load_garden_model("awareness_structural_mmm")
→ fit_mmm_model(…)
Fitted. The model's declared estimands are ready — awareness lift and contribution ROI.
You Show me the headline numbers and build a report.
Agent → get_estimands() · → generate report
Awareness lift +6.2pp (89% HDI 3.1–9.4), contribution ROI 2.3× … report attached.

What is actually happening under those tool calls:

  1. Discoverlist_garden_models returns the org's catalog (name, version, status, compatibility score, docs).
  2. Loadload_garden_model stages the model into the session: it copies the source into the workspace, sets spec["garden_ref"], and applies the model's recommended_fit to the inference settings.
  3. Fitfit_mmm_model reads the spec and dispatches to the kernel, where the build step resolves the garden class from garden_ref, validates any model_params against the model's config schema, wires the declared likelihood and estimands, and fits the bespoke subclass.
  4. Interrogateget_estimands realizes 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.
  5. 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

ToolRoleWhat it does
list_garden_modelsanyBrowse the org's catalog (name, version, status, compatibility, docs).
load_garden_modelanyStage a model into the session spec via garden_ref + recommended fit.
fit_mmm_modelanyFit the staged model (resolves the garden class kernel-side).
get_estimandsanyRealize the model's declared estimands (mean + HDI + units).
register_garden_modelanalyst+Register new source as a draft (AST-validated, not executed).
test_garden_modelanalyst+Run the compatibility suite; auto-promote draft → tested on a clean pass.
publish_garden_modeladmin/ownerPromote 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:

The authoring loop in the UI

  1. New model. The Code editor seeds a CustomMMM starter; name the model.
  2. 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.
  3. Ask the copilot. The inline Bayesian-modelling assistant streams answers; Apply to editor merges a suggested code block straight into the buffer.
  4. Document. Write the model's markdown docs in the Docs tab with live preview.
  5. Register. Register draft stores the source (AST-validated, not executed) as version 1, status draft.
  6. 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.
  7. Prove. Run compatibility test streams the tier report; on a clean blocking pass the model auto-promotes to tested and a Publish button appears.
  8. 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 pass

Tested

All blocking tiers pass on synthetic worlds. Editable docs.

human · publish gate

Published

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

StageWhenChecks
validate_classStatic (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_instancePost-initRequired attributes are present and sane; for MMM models, channel_names is a non-empty ordered list and _media_raw_max covers every channel.
validate_fittedPost-fitThe 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.

TierBlockingWhat it provesNon-MMM
staticyesvalidate_class passes (structure, methods, fit signature).runs
buildyesThe class instantiates on a synthetic world via the agent's build step.runs
fityesA MAP fit completes and sets the trace.runs
instanceyesvalidate_instance passes (attributes, channel/media coverage).runs
traceyesvalidate_fitted passes (posterior structure).runs
scalingyesOriginal-scale predictions are finite and within 0.01–100× of the KPI mean.skipped
ops_smokeyesFive oracle read-ops (ROI, decomposition, diagnostics, adstock, saturation) smoke-test cleanly.skipped
carryoveroptionalOn a 2-geo panel, a spend spike in one geo does not bleed to the other (per-cell adstock).optional
accuracyoptionalRecovered 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.

Author's checklist

  • State the identification strategy in the docstring.
  • Put every tunable in CONFIG_SCHEMA with 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.

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 & pathRolePurpose
POST /model-gardenanalyst+Register source as a draft (AST-validated, not executed).
POST /model-garden/lintanalyst+Static Problems check for the editor (syntax, class, conventions).
POST /model-garden/formatanalyst+Ruff/Black format the editor buffer.
POST /model-garden/copilotanalyst+Streaming (SSE) Bayesian-modelling copilot; stateless.
GET /model-gardenanyList the org's models (latest per name by default).
GET /model-garden/{name}/versionsanyEvery version of one model, newest first.
GET /model-garden/{name}/{version}anyOne version, with manifest + compatibility report.
GET /model-garden/{name}/{version}/sourceanyStored source text for the Atelier editor.
PATCH /model-garden/{name}/{version}analyst+Edit docs in place (409 if published).
POST /model-garden/{name}/{version}/testanalyst+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}/promoteadmin/ownerHuman publish gate (tested → published).
DELETE /model-garden/{name}/{version}admin/ownerDelete 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.

FieldMeaning
contract_versionThe garden contract the model was written against.
class_nameThe authored class to reconstruct (AST-detected).
model_kindThe family (mmm, cfa, latent_class, …) for discovery/filtering.
config_schemaJSON Schema of CONFIG_SCHEMA for the dynamic params form.
default_estimandsWhat the model claims to measure.
recommended_fitInference settings load_garden_model applies by default.
capabilities · tags · dataset_schemaDiscovery hints, search tags, and the expected data shape.
demo_notebookOptional 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:

  1. Read the three example models under examples/garden_models/awareness_structural_mmm.py, bayesian_cfa.py, bayesian_lca.py — and fork the closest one.
  2. Open the Atelier, paste it into the Code tab, and tweak the structure with the copilot.
  3. Upload a dataset in the Notebook tab and sanity-check the fit on real data.
  4. Run the compatibility test; fix anything that isn't green.
  5. 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.