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
@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| Field | Notes |
|---|---|
id | Numeric 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_type | The string registered by an EXPERIMENT_DESIGN plugin |
status | Usually planned, ongoing, completed, or cancelled |
tags, custom_metadata | Free-form JSON columns plugins can read but generally should not mutate unless their plugin type owns the experiment update path |
parent_experiment_id | For nested experiments / sub-runs |
DesignData
The design-plugin payload for one experiment.
@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: datetimedata 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.
@dataclass(slots=True)
class PluginAnalysisResult:
id: int
experiment_id: int
plugin_id: str
result: dict[str, Any]
created_at: datetime
updated_at: datetimeThe 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:
await plugin.save_analysis(experiment_id, {
"runs": [
{"id": "2026-05-01T10:00:00Z", "summary": {...}},
{"id": "2026-05-01T14:00:00Z", "summary": {...}},
],
})User
@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 = Nonerole is the platform role — Admin, Member, Viewer, or a custom-role name. Plugin roles are tracked separately as UserPluginRole.
UserPluginRole
@dataclass(slots=True)
class UserPluginRole:
id: int
user_id: int
plugin_id: str
role: str
created_at: datetime
updated_at: datetimerole 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
| Repository | Returns | Writes |
|---|---|---|
ExperimentRepository | Experiment | Experiment (EXPERIMENT_DESIGN and FULL plugins) |
PluginDataRepository.save_experiment_data | DesignData | DesignData |
PluginDataRepository.save_analysis_result | PluginAnalysisResult | PluginAnalysisResult |
PluginDataRepository.get_analysis_results | list[PluginAnalysisResult] (calling plugin by default; pass include_others=True for every plugin's result on one experiment) | — |
UserRepository | User | — |
PluginRoleRepository | UserPluginRole, `str | None` (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:
- Within
DesignData.data/PluginAnalysisResult.result— JSON. Quick and schema-flexible. Best for plugin-specific configuration and outputs. - 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