Skip to content

Data model

The mint-sdk data classes mirror the platform's core entities — but exposed as immutable dataclasses with the fields plugins typically read or write. Repositories return these dataclasses; the platform owns the underlying SQLAlchemy models.

Entities

Experiment

python
@dataclass(slots=True)
class Experiment:
    id: int
    name: str
    experiment_type: str
    status: str
    created_at: datetime
    updated_at: datetime
    created_by: int | None = None
    parent_experiment_id: int | None = None
    project: str | None = None
    notes: str | None = None
    tags: dict = field(default_factory=dict)
    custom_metadata: dict = field(default_factory=dict)
    start_date: date | None = None
    end_date: date | None = None
FieldNotes
idNumeric primary key. The user-facing experiment_code (LCM-EXP-001, DR-EXP-001, …) is not on the SDK dataclass — it's a platform-side field exposed via the REST API
experiment_typeThe string registered by an EXPERIMENT_DESIGN plugin
statusUsually planned, ongoing, completed, or cancelled
tags, custom_metadataFree-form JSON columns plugins can read but generally should not mutate unless their plugin type owns the experiment update path
parent_experiment_idFor nested experiments / sub-runs

DesignData

The design-plugin payload for one experiment.

python
@dataclass(slots=True)
class DesignData:
    id: int
    experiment_id: int
    plugin_id: str
    data: dict[str, Any]
    schema_version: str
    created_at: datetime
    updated_at: datetime

data is whatever JSON your design plugin defines. schema_version defaults to the value in PluginMetadata.schema_version — bump it when your design schema changes incompatibly.

PluginExperimentData is a backward-compatible alias for DesignData.

PluginAnalysisResult

The output of one analysis-plugin run on one experiment.

python
@dataclass(slots=True)
class PluginAnalysisResult:
    id: int
    experiment_id: int
    plugin_id: str
    result: dict[str, Any]
    created_at: datetime
    updated_at: datetime

The current platform stores analysis results as JSON entries keyed by plugin_id on the experiment row. Saving a new result for the same (experiment_id, plugin_id) updates that entry; it does not append a separate run row. To preserve history, embed a per-run sub-key inside result:

python
await plugin.save_analysis(experiment_id, {
    "runs": [
        {"id": "2026-05-01T10:00:00Z", "summary": {...}},
        {"id": "2026-05-01T14:00:00Z", "summary": {...}},
    ],
})

User

python
@dataclass(slots=True)
class User:
    id: int
    username: str
    role: str
    is_active: bool
    created_at: datetime
    updated_at: datetime
    email: str | None = None
    shortname: str | None = None
    first_name: str | None = None
    last_name: str | None = None

role is the platform role — Admin, Member, Viewer, or a custom-role name. Plugin roles are tracked separately as UserPluginRole.

UserPluginRole

python
@dataclass(slots=True)
class UserPluginRole:
    id: int
    user_id: int
    plugin_id: str
    role: str
    created_at: datetime
    updated_at: datetime

role is whatever string your plugin defines. Plugin role checks are performed by PlatformContext.require_plugin_role(*roles).

Relationships

Project ────< Experiment ──────── DesignData          (one design payload per experiment)

                  └──────────────── PluginAnalysisResult  (one entry per plugin id)

                  └────────────────< (plugin-owned tables, via shared_db_session)

User ──────< UserPluginRole              (one per (user, plugin))

The platform owns Project, Experiment, User, DesignData, PluginAnalysisResult, and UserPluginRole. In the current backend, design data and analysis results are JSON columns on the experiment row and the SDK adapter returns SDK-shaped objects for plugin code. Plugin-owned tables live in the plugin's own Postgres schema (integrated mode) or its own SQLite database (standalone mode).

JSONB portability

DesignData.data and PluginAnalysisResult.result are JSON-typed columns. Postgres uses native jsonb (queryable, indexable); SQLite uses serialized JSON in a TEXT column. The repository layer abstracts the difference. Code that just reads / writes whole dicts works in both backends.

For complex queries (e.g., "find experiments where result.method == 'v4'"), prefer a real column inside a plugin-owned table over JSON-key indexing — JSON expression indexes work but reduce portability.

What the repositories return

RepositoryReturnsWrites
ExperimentRepositoryExperimentExperiment (EXPERIMENT_DESIGN and FULL plugins)
PluginDataRepository.save_experiment_dataDesignDataDesignData
PluginDataRepository.save_analysis_resultPluginAnalysisResultPluginAnalysisResult
PluginDataRepository.get_analysis_resultslist[PluginAnalysisResult] (calling plugin by default; pass include_others=True for every plugin's result on one experiment)
UserRepositoryUser
PluginRoleRepositoryUserPluginRole, `strNone` (a single role)

See the API Reference → Python SDK for the full method list.

Extending the model

Plugins extend the data model in two complementary ways:

  1. Within DesignData.data / PluginAnalysisResult.result — JSON. Quick and schema-flexible. Best for plugin-specific configuration and outputs.
  2. Plugin-owned tables — declare via get_shared_models() and/or migrations. Best for queryable, relational data the plugin owns end-to-end.

Pick (1) when the data is tightly coupled to one experiment and never queried across experiments by anyone else. Pick (2) when you need indexes, cross-experiment queries, or relational integrity.

Next

Migrations — evolving plugin-owned tables safely → PlatformContext — accessing repositories → Recipes → Querying plugin data — patterns for plugin-owned tables

MINT is open source. Made by the Morscher Lab.