parleml Analyst — API reference¶
The endpoints behind the MCP tools, with response shapes and one realistic
JSON example each — the reads in detail, the builder routes in summary. All
paths are under the backend base URL (PARLEML_API_BASE, default
http://localhost:8000).
Contents¶
- Auth, scopes & status codes
- Navigation
data-kb(queryable tables)results-catalog(per-experiment schema)query(read-only SQL) andprofile- Dataset reads (EDA, leakage, columns)
- Dataset probes (compute scope)
- Model evaluation, insights & per-row cases
- Run observability
- Comparisons
- Platform vocabulary
- Capabilities (per workload)
- Annotations (AI-written text)
- AutoDoc
- Builder routes (summary)
- Charts
- Caps & env knobs
Auth, scopes & status codes¶
- Bearer token via
PARLEML_API_TOKEN(Authorization: Bearer …), required only when the backend runs withPARLEML_AUTH_REQUIRED=true. Mint under Settings → Access orPOST /api/settings/tokens./api/healthis always open. - Token scopes:
analyst(default — every read GET plus the read-shaped POSTs/query,/profile,/data-kb,/api/experiments/compare,/api/models/compare),compute(adds the bounded compute POSTs: the dataset probes, the dataset drift comparison, row scoring and the decision simulations) orbuilder(adds the builder routes inside the workspaces the token is bound to: plans and their launches, abort, replicate, explanations, AutoDoc rebuilds, publishing, batch scoring, column roles, EDA, leakage scans and triage, recipes, model registration and predictions import — each with anIdempotency-Key). Scopes gate tokens in every auth mode; people in the UI are never scope-gated. Nothing a token can reach promotes, deletes or changes settings. - Status codes:
200ok ·400bad request (empty workspace, non-SELECT SQL, unknown table/column) ·401missing/expired/revoked token when auth required ·403the token's scope (or workspace binding) does not cover the route ·404unknown id / experiment not published ·409it conflicts with the current state (a stale plan, anIdempotency-Keystill in use or used for another request) ·413upload too large ·422a malformed field ·429the builder token's run budget is spent. Error body:{code, message, details?}.
Navigation¶
GET /api/workspaces→[{workspace_id, name, …}];GET /api/workspaces/{id}.GET /api/datasets?workspace_id=<id>→[{dataset_id, workspace_id, name, …}];GET /api/datasets/{id}.GET /api/experiments?workspace_id=<id>→[{id, name, type, status, …}];GET /api/experiments/{id}→ detail incl.type,status,target,dataset_id.type∈supervised | external | cluster | anomaly | forecast | recommendation | ltr | association.GET /api/models(a detail,GET /api/models/{id}, carries the stage, lineage, versions and serving endpoints),GET /api/knowledgebases?workspace_id=<id>,GET /api/connections,GET /api/templates. The MCPget_resourceaddsresource_kindto every detail and leaves out server paths (outputs_dir,canonical_path,artifact_uri).- A plan:
GET /api/experiments/plans/{id}. The workspace brief:GET /api/workspaces/{id}/brief(get_workspace_brief). list_resources("jobs", workspace_id=…)readsGET /api/experimentsand returns the experiments still pending or running as job handles ({job_id, kind, status, poll_after_s, ui_url}) to poll withget_run.
data-kb — queryable tables¶
GET /api/workspaces/{id}/data-kb → a KnowledgeBase (a pure read — 404 until
first materialised, and it returns the stored object list as-is). POST on
the same path get-or-creates and reconciles with the live workspace.
The MCP describe_catalog tool always POSTs, because only that path picks up
newly registered datasets and freshly published results — a GET would show a
stale catalog. It materialises a rebuildable index, never user data, which is
why /data-kb is allow-listed as a read-shaped POST. Call it first to learn
the real SQL view names and join edges. With experiment_id it narrows to that
experiment's published tables (see results-catalog below).
{
"tables": [
{
"name": "insurance_claims_fraud",
"is_result": false,
"dataset_id": "ds_1",
"row_count": 6000,
"primary_key": [],
"columns": [
{
"name": "region",
"data_type": "VARCHAR",
"nullable": true,
"is_primary_key": false
},
{
"name": "claim_amount",
"data_type": "DOUBLE",
"nullable": true,
"is_primary_key": false
}
]
},
{
"name": "insurance_claim__predictions",
"is_result": true,
"dataset_id": "ds_9",
"row_count": 6000,
"primary_key": ["row_index"],
"columns": [
{
"name": "row_index",
"data_type": "BIGINT",
"nullable": false,
"is_primary_key": true
},
{
"name": "predicted_proba",
"data_type": "DOUBLE",
"nullable": true,
"is_primary_key": false
},
{
"name": "predicted_class",
"data_type": "BIGINT",
"nullable": true,
"is_primary_key": false
}
]
}
],
"relationships": [
{
"id": "r1",
"from_table": "insurance_claim__rules_fired",
"from_columns": ["row_index"],
"to_table": "insurance_claim__predictions",
"to_columns": ["row_index"],
"kind": "fk",
"status": "approved",
"confidence": "fk"
}
]
}
confidenceis an enum —fk | high | medium | low, not a number. Filter join edges withconfidence in ('fk','high'); the numeric score, when present, is the separate optionalprobability.tables[].nameis the SQL identifier (use inquery).data_typeis a DuckDB type string (BIGINT,DOUBLE,VARCHAR,INTEGER,TIMESTAMP_NS,BOOLEAN, …).dataset_idis the join toresults-catalog.is_resultseparates published results (true) from input datasets (false).describe_catalog(workspace_id, preview="<object>")returns the first 20 rows of one object throughGET /api/knowledgebases/{kb_id}/object-preview?object=…&n=20— a lakehouse table by default, or withknowledgebase_ida source table of that knowledge base (what a recipe can select from).
results-catalog — per-experiment schema¶
GET /api/experiments/{id}/results-catalog → a ResultsCatalog. 404 before
the experiment is published. It holds the roles, column meaning, and join keys,
under canonical table names.
The MCP reads it through describe_catalog(workspace_id, experiment_id=…), which
also POSTs data-kb and joins the two on dataset_id. It returns
{experiment_id, experiment_type, profile, tables, relationships, note}: each
table is the entry below with table renamed canonical_id, plus name (the
SQL view name from data-kb; null until the table is in the lakehouse) and
row_count. relationships are the data-kb join edges that touch those tables.
{
"experiment_id": "exp_ab12",
"experiment_type": "supervised",
"profile": "binary",
"tables": [
{
"table": "exp_ab12_predictions",
"suffix": "predictions",
"role": "fact",
"scope": "full",
"dataset_id": "ds_9",
"primary_key": ["row_index"],
"columns": [
{
"name": "row_index",
"dtype": "int",
"role": "key",
"description": "Positional join key…"
},
{
"name": "predicted_proba",
"dtype": "float",
"role": "measure",
"description": "P(positive)…"
},
{
"name": "predicted_class",
"dtype": "int",
"role": "measure",
"description": "Predicted class 0/1"
}
],
"foreign_keys": [
{
"column": "archetype_id",
"references_table": "exp_ab12_archetypes",
"references_column": "archetype_id",
"cardinality": "many_to_one"
}
],
"provenance": {
"experiment_id": "exp_ab12",
"result_kind": "binary",
"row_index_basis": "source_row_order"
}
}
]
}
tableis the canonical name — never used in SQL (canonical_idin the MCP result).columns[].dtypeis the logical type (int | float | str | bool), distinct from the DuckDBdata_typereported bydata-kb.foreign_keys[].references_tableis the canonical name of the target table; the MCP result leaves it canonical, so match it to acanonical_idand use that table'sname.
query — read-only SQL¶
POST /api/workspaces/{id}/query body { "sql": "<one SELECT>", "limit": 200? }
→ {columns, rows, n_returned, has_more}.
{
"columns": ["region", "n"],
"rows": [
{ "region": "NE", "n": 1203 },
{ "region": "SW", "n": 880 }
],
"n_returned": 2,
"has_more": false
}
- Exactly one read-only
SELECT; no DDL/DML/multiple statements (→ 400). - Empty workspace (no datasets/results) → 400.
limitonly lowers the server cap (agent_max_rows, default 1000); userLIMIT/aggregation in the SQL is preserved.has_more=true⇒ truncated.
profile — one-call table triage¶
POST /api/workspaces/{id}/profile body { "table": "<data-kb name>",
"columns": ["x"]? } → {workspace_id, table, row_count, columns:[SUMMARIZE
rows: column_name, column_type, min, max, null_percentage, …, n_distinct,
n_distinct_exact], top_values: {col: [{value, n}]}}. n_distinct is exact
(n_distinct_exact=true) on tables up to 2,000,000 rows and a HyperLogLog
estimate above that — never read an estimate as duplicates on a key column.
Table names are validated against the
workspace index (400 lists the queryable names); top values ship only for
low-cardinality columns. The pre-SQL read — use before writing discovery SQL.
Dataset reads (EDA, leakage, columns)¶
GET /api/datasets/{id}/eda→ the cachedEDAResult(table stats, per-columnvariables,alerts; 404 until computed —prepare_data(action="eda")computes it).get_eda(section="summary")hands the panel the whole summary and the model a digest: alert descriptions (not the profiler's per-alert details), the top target correlations, group differences and outliers, missingness and the number of findings the leakage scan raised (leakage_scan_findings;section="leakage"is their triage). Per-column drill:GET /api/datasets/{id}/eda/variable/{column}.GET /api/datasets/{id}/leakage→ theLeakageReport(target-leakage scan; 404 untilprepare_data(action="leakage")or the EDA ran it). Shape findings (high_cardinality,id_likeness) on a column whose declared role explains them — a key or record id, a base time, a domain (incl. a packedmtlist) or free text — are listed underrole_explained(with the role) and are not counted: set the roles before triaging, and there is nothing to confirm for them. So is a finding of any kind on an ignored column (not a feature: it cannot reach the model). Asingle_valuefinding (a constant column, such as a single-series forecast's series key) is data hygiene, never a leak — a constant carries no information: it is listed underhygiene, not counted, and nobody triages it. A finding a person confirmed on another dataset of the same recipe lineage (same kind and columns, same target and time column) reads as confirmed here, withcarried_fromnaming that dataset.- The EDA profile and the leakage scan run as a worker job on the server, one per dataset
at a time. The MCP sends
?wait_seconds=20; work still queued or running then answers202{status: "queued" | "running", wait_handle: "eda:<dataset_id>", poll_after_s, queue, cancel_route}. In a session ParleML runs, wait on the handle with the session toolawait_operation, then read the result withget_eda. Other work on a dataset whose EDA is running is a409naming the same handle. One that runs far longer than it should is stopped withprepare_data(dataset_id, action="cancel")(POST …/eda/cancel, thecancel_route); one that makes no progress at all is failed by the server on its own afterPARLEML_JOBS_STALL_AFTER_S(15 min): its waiters get503 job_stalled. - A recipe materialise (
author_recipe(action="materialize" | "refresh")) scans the new dataset for leakage and answersleakage_high_severity; when that scan did not finish (failed, stalled or stopped) it isnullandleakage_scan_errorsays why — scan again withprepare_data(action="leakage")before planning. GET /api/datasets/{id}/columns/evidence→ColumnSuggestionsResponsewithout any model:evidence— per column its type, samples, EDA statistics (once EDA ran) and, for a string column,value_checks({domain, match_rate, delimiter}: the share of sampled values that parse as each column domain, best first) andprose_rate— plus the current overrides, and thesuggestionsthe checks support (source: "checks"). The Columns tab's AI-written suggestions are the dataset'scolumns.describeannotation, written by the workspace's agent from the same evidence (see Annotations).get_eda(dataset_id, section="columns")reads it, with each column's declared roles (target, record id, key, ignored, fairness, base time, domain) inline.GET /api/datasets/{id}/eda/text-columns→ the columns detected as free text (get_eda(dataset_id, section="text")).
Dataset probes (compute scope)¶
Bounded single-shot computation before anything is trained. The MCP
probe_dataset(dataset_id, kind, params?, refresh?) posts to one of these
routes under /api/datasets/{id} and adds probe: <kind> to the result. It
rejects a missing or unknown param before the call, naming the allowed ones
(? marks an optional param).
signal→POST /signal-check. Params:target(sent astarget_col),task? (auto,binary_classification,multiclass_classificationorregression). Returns the verdict, drivers and leakage smell (SignalCheckResult). Its holdout evaluation (model_evaluation) reaches you as each split's metric tiles and confusion matrix: the dataset page (ui_url) draws the curves and, for a multiclass target, each class's one-vs-rest slice. A wide dataset's column ledger (decisions) reaches you as the entries that fit, the columns the probe excluded or encoded first (decisions_totalsays how many there are). Cached per target/params;refresh=truerecomputes.- Both signal checks run once per question: a second identical request
joins the running computation instead of starting another. The MCP sends
?wait_seconds=20; a check still computing then answers202{status: "queued" | "running", wait_handle: "probe:<kind>/<dataset_id>/<digest>", poll_after_s, queue}(queuewhile it waits for a slot: why, and how far back). In a session ParleML runs, the session tool await_operation (operation_id = thewait_handle) holds until the result is cached (then the same call returns it at once); elsewhere, repeat the same call afterpoll_after_s. forecast_signal→POST /forecast-signal-check. Params:time_col,target_col,series_id_col?,horizon?,resample_to?,resample_agg? (server defaultsum). Returns the verdict, the zero-shot Chronos vs SeasonalNaive MASE and skill, the backtest curves, andgrid. Cached;refresh=truerecomputes.baseline_floor→POST /baseline-floor. Params:target,task(one of the three tasks — noauto, because the floor depends on it),metric(anEvalMetricname, sent aseval_metric),positive_class? (binary only). Returnsstrategy(the strongest feature-blind predictor) and itsscorein the metric's own units — what a trained model has to beat. Not cached.temporal_split→POST /temporal-split-preview. Params, in the plan's own words (a temporalexperiment.validationblock):time_column,temporal_boundary_mode? (fraction, the default, ordate),holdout_frac? (the test share, default 0.2),outer_test_cutoff? (required indatemode: the test starts after it),validation_cutoff? (datemode only, optional: where the calibration period starts — without it, the lastvalidation_holdout_fracof the rows before the test),validation_holdout_frac? (config.engine.validation_holdout_frac, default 0.1) andembargo? (an integer +H,D,Wormin, e.g."60D"). The plan's validators apply, so what the preview accepts the plan accepts (a refusal is a 422 naming the field). Returns the exact chronological partition a temporal run would use: rows and time range of train / selection / calibration / test, the rows the gap excludes, warnings, andlaunch—validationto paste into the plan'sexperiment.validationandvalidation_holdout_fracintoconfig.engine.validation_holdout_frac. Not cached.
Reading the forecast probe¶
The forecast probe always answers. grid: "ordinal" means the timestamps had no
regular spacing, so the series were scored in observation order: the verdict is
about the next N observations, not N days, and calendar seasonality is
invisible on that axis — so a weak ordinal verdict is not evidence of no signal.
Re-run with resample_to (a pandas alias: 'D', 'W', 'ME') to aggregate
onto a real grid; resample_agg is 'sum' for flows (counts, sales) and
'mean' for levels. horizon defaults from the data when omitted.
Other compute routes¶
POST /api/experiments/{id}/predictbody{ "row": {…}, "calibrated": true }→ composed row scoring (prediction + drivers + archetype + neighbours) for ONE raw row — thewhat_iftool on a supervised run. Withexplain=true,POST /api/experiments/{id}/predict/local(same body) adds the local sensitivity: ICE curves, LOCO deltas and the archetype counterfactual. On an anomaly or clustering run,POST /api/experiments/{id}/unsupervised/predictbody{ "rows": [ {…} ], "include_reasons": true }scores the row and gives its reason codes. The MCP stampsscoring(supervised,supervised_localorunsupervised) on the result.- "What if this case's X were Y": every one of those bodies takes
base—{"row_index": n}(therow_indexof the run's published tables) or{"key": {"claim_id": "…"}}(the dataset's declared key; exactly one row must match) — and thenrowholds only the columns that change ({}scores the case as it is). Withoutbase,rowis the whole case and a column the model reads that it omits is sent as missing (null) — the model's own missing-value handling then decides what that means (AutoGluon reads a missing integer column as 0), so a real case is better named bybase. The answer'srow_basis(a list of them for the unsupervisedrows) says what was scored:base_row_index,replaced(the base row's own values of the columnsrowset),nulled_columns(sent as missing) andignored_columns(given, but not read by the model — changing them changes nothing). POST /api/datasets/compare— dataset drift, see Comparisons.- The decision simulations under
/api/experiments/{id}—/business-value,/multiclass-cost,/decision-layer/preview,/ranking/at,/recommendation/business-value,/ltr/business-value— take the same scope: each computes an answer and changes no model, dataset or operating point.
Model evaluation, insights & per-row cases¶
GET /api/experiments/{id}/mli/model-evaluation→MLIModelEvaluation: per-split metrics + ROC/PR/calibration/lift/gains/KS curves + confusion matrix + per-thresholdconfusion_curve. 404 = no holdout evaluation.- Computed insights — one GET each under
/api/experiments/{id}:/leaderboard,/scores,/diagnose,/decision-layer,/trust,/slices, the/mli/*explanation reads (overview, fairness, ALE, error analysis and error tree, drift, text insight, label noise, SHAP interactions, engineered features, surrogate, glass box, native trees, field descriptions, the archetype landscape and archetypes, cases), and the/forecast/*,/unsupervised/*,/association/*,/recommendation/*,/rankingand/ltr/*summaries. The MCPget_insighttool wraps the curated set as named kinds:get_insight(id, "kinds")lists the ones that apply to an experiment; their meanings are inexplainability.md. GET /api/experiments/{id}/mli/cases/{row_index}/neighbours?top_k=5→ the SHAP-space neighbourhood of one row —get_insight(id, "case", {"row_index": N, "top_k": 5}).
Run observability¶
Everything about one run, under /api/experiments/{id}. The MCP
get_run(experiment_id, view?, since?, limit?) composes these reads, one
view at a time:
summary(default) — the detail,/run-all(404 = no Run-all chain was started),/notificationsand/mli/status: the status,running, the current stage, the stage list (status, duration, error, skip reason), the Run-all chain state, the explanation status, the latest 5 notifications, the error, and anexthint.stages/pipeline—/stages//pipeline: the stage list; the stage graph.events—/events?since_seq=N: the lastlimitevents (default 50);next_seqis the cursor to pass back assince, andrestartedsays the event log started over.log—/log?tail=N: the console's lastlimitlines (default 80), withearlier_outputwhen more exists. Read it when a run failed.notifications—/notifications: the warnings and notices the run raised (the lastlimit, default 100).settings/config—/run-settings: the labelled launch settings; orresolved_config(the config the run used) withresolved_provenance.explanations—/mli/statusand/mli/pipeline: the Explain step's status and stage graph.downloads—/downloads: the files the run produced (kind, file name, size). Names only — the files themselves download from the parleml UI.
GET /api/experiments/{id}/log?offset=N streams the console forward from byte
N, one bounded chunk per call. ?tail=N instead returns the last N bytes,
cut forward to whole lines: the response's offset is where that window starts
and next_offset the file size, so a reader can keep polling from there.
Comparisons¶
POST /api/experiments/comparebody{ "ids": [1–4 ids of one workspace], "include_production_model": false }→ExperimentComparison:rows(identity, primary metric, validation / test score, per-split metric strips, trust grade and flagged tiles, up to 10 top features,unavailablereasons),metrics(one entry per metric × split:valuesaligned withrows, thebestrow indices,higher_is_better; largest relative spread first),config_diff(the dotted resolved-config paths whose values differ),production_model(the workspace's model in stage Production, the latest to enter it; its experiment's row saysis_production_model) andnotes. Read-shaped: anyanalysttoken. MCP:compare("experiments", ids, include_production_model=True). It reads each run's explanation summary — written whenever its explanations are saved — and never loads a profile: a run explained before the summary existed says so inunavailable(explain it again).POST /api/models/comparebody{ "ids": [2–4 registered model ids] }→ lineage and headline scores, in request order. Read-shaped. MCP:compare("models", ids).POST /api/datasets/comparebody{ "reference_id": …, "current_ids": [1–3 ids] }→ per current dataset, PSI per feature with drift tiers; the reference fixes the bins. Compute scope. MCP:compare("datasets", ids, reference_id=…), whose text keeps the 20 highest-PSI features per comparison (n_featuresis the full count; the panel gets them all).GET /api/datasets/compare/feature/{feature}?reference_id=…¤t_id=…→ one feature's bins (or categories) side by side —compare("datasets", [id], reference_id=…, feature=…).- The comparison panels name metrics with the metric catalog
(
/api/describe/metric-catalog), which the MCP attaches to the panel's view of the result only.
Platform vocabulary¶
Static catalogues — what ParleML offers to choose from. The MCP
describe_platform(kind, name?, task?, context?) reads them (all GET):
kind |
Route |
|---|---|
presets |
/api/describe/presets |
templates |
/api/templates (name → /api/templates/{name}) |
algorithms |
/api/describe/algorithms?task= |
metrics |
/api/describe/metrics?task= |
metric_catalog |
/api/describe/metric-catalog?context= |
foundation_models |
/api/describe/foundation-models |
foundation_engines |
/api/foundation-engines |
transformers |
/api/describe/transformers |
domains |
/api/describe/domains |
config_schema |
/api/describe/ml4-config (name → /api/describe/config-schema/{name}) |
plan_schema |
/api/describe/plan-schema (name → /api/describe/plan-schema/{name}) |
forecast_models |
/api/forecast/models |
detectors |
/api/unsupervised/detectors (name → /api/unsupervised/detectors/{name}) |
clustering_algorithms |
/api/unsupervised/algorithms/clustering |
explain_presets |
/api/mli/presets |
task filters algorithms / metrics: binary_classification,
multiclass_classification or regression (the vocabulary every request uses; anything
else is a 422 naming these). context is the required filter of metric_catalog:
supervised, forecast, clustering, anomaly, recommendation, ranking or
association. config_schema without name returns the list of config
sections; with name, that one section's JSON Schema — the supervised config only.
plan_schema is every other plan block. name ∈ supervised, validation, forecast,
association, cluster, anomaly, recommendation, ltr returns plan_field (where the
block sits in run_experiment(action="plan") params, e.g. experiment.unsupervised),
fixed (what the block needs elsewhere: experiment.type, unsupervised.task), paths
(every setting's dotted path inside the block — clustering.auto_k_min,
unsupervised.passive_columns) and json_schema: the JSON Schema of the very model the plan
validates against (types, enums, defaults, descriptions). A setting it does not list is one
the plan refuses — offer and plan only what it lists. Without name: the blocks and where
each sits. An unknown name or section is a 404 that lists the valid ones.
explain_presets lists the explanation depths with what each computes: shallow (exact SHAP,
archetypes, EDA), standard (+ rules, error tree, surrogate, ALE, reduced permutation),
standard_rules (run_all's default: + the applyable business-rule model) and deep (+ PDP,
SHAP interactions, the residual surrogate, archetype stability and a longer business-rule
search). run_experiment takes one as explain_preset — on a plan for run_all's explain
stage, and on action="explain".
Capabilities (per workload — NOT a single route)¶
There is no /api/experiments/{id}/capabilities. Route by type:
type |
Endpoint |
|---|---|
supervised, external |
GET /api/experiments/{id}/mli/capabilities |
forecast |
GET /api/experiments/{id}/forecast/capabilities |
cluster, anomaly |
GET /api/experiments/{id}/unsupervised/capabilities |
recommendation, ltr, association |
(no capabilities route) |
The MCP get_capabilities tool does this routing for you and returns a graceful
note for the unmapped/not-computed cases. It also adds explanations
({state, error} of the Explain step, GET /api/experiments/{id}/mli/status)
and insight_kinds / chart_kinds: the get_insight kinds and
get_chart_data charts for the experiment's type, split into ready and
needs_explanations — or all under after_the_run_succeeds while the run is
not done.
Annotations (AI-written text)¶
AI-written text in ParleML is an annotation on a finished object, written by
the workspace's agents (the same CLIs and sign-ins as sessions), never by a fit,
an EDA or an explanation compute. Each carries who wrote it: agent, model,
at and the writer (session, person or automatic).
- Read them in the digests, no extra call.
get_eda(section="summary")(eda.summary,columns.describe),get_resourcefor a dataset, experiment or knowledge base,get_run(view="explanations")(explain.narration,explain.fields,explain.rule,explain.class,clusters.names) anddescribe_catalogfor a knowledge base (kb.describe,kb.relationships,recipe.explain) addannotations: {entries: [{task, item?, value, written_by, accepted?, regenerating?}], note}when the object has any. Text over 2,000 characters is cut. - They are not measurements. Quote one only as AI-written, and check it
against the object's own numbers first. A suggestion (column roles, table
descriptions, relationships) is a proposal until someone accepts it;
acceptedlists the items already taken. - Agents do not ask ParleML to write them.
POST /api/annotations/…andGET /api/annotations/statusrefuse every API token (403): an agent is already a model and writes its own text. A session's agent takes a column suggestion by writing the same value throughprepare_data(action="columns"); that is refused (403) unless the workspace's profile lets agents accept AI suggestions, and recorded as accepted in that session when it is allowed. GET /api/{datasets|experiments|knowledgebases|workspaces}/{id}/annotations→ every annotation on the object, with whether one is being written. A workspace whose profile turns AI annotations off returns none, and its digests carry none.
AutoDoc¶
GET /api/experiments/{id}/autodoc/render?fmt=md|html → the model card.
md/html are readable text; use as orientation before SQL.
get_autodoc(id, fmt="json") reads GET /api/experiments/{id}/autodoc?include_figures=false
— the card's sections as structured fields, without the figures. ai_written lists the
sections holding AI-written text with their label (AI-written · Claude Code · haiku · 8 Oct);
empty means the card has none. A stale card is built by a
worker job first: past ?wait_seconds=20 the call answers 202 with
wait_handle: "autodoc:<experiment_id>" — wait on it with the session tool
await_operation, then call get_autodoc again. run_experiment(action="autodoc")
(POST …/autodoc/rebuild) answers the same way. (The render
endpoint also serves fmt=docx, but the get_autodoc tool rejects it — the
binary docx is a UI-only download.)
Builder routes (summary)¶
A builder token reaches these, inside the workspaces it is bound to, each
with an Idempotency-Key (the MCP derives one per call and returns it; a
retry with the same key replays the first response, 409
idempotency_key_in_use while it still runs). The tools' docstrings and
get_playbook("deliver") are the reference:
| Tool | Routes |
|---|---|
prepare_data |
PUT /api/datasets/{id}/columns, POST …/eda, POST …/leakage, PUT …/leakage/findings/{finding} |
author_recipe |
POST /api/knowledgebases/{id}/recipes, PATCH …/recipes/{r}, POST …/iterate, …/materialize, …/refresh (reads: GET …/recipes/{r}, …/preview) |
run_experiment |
POST /api/experiments/plan, POST /api/experiments/plans/{plan}/launch, POST /api/experiments/{id}/abort, …/replicate, …/mli, …/autodoc/rebuild, …/publish-results (read: GET …/publishable-results) |
score_batch |
POST /api/experiments/{id}/predict-batch (anomaly / cluster: …/unsupervised/predict-batch), POST …/{job}/cancel (reads: the job, the list, and for supervised jobs …/evaluation, …/ranking, …/rows) |
register_model |
POST /api/models/register (with its release_record), PUT /api/models/{id}/release-record / PUT /api/experiments/{id}/release-record (action="record_release"), POST /api/workspaces/{id}/models (a predictions CSV) |
update_workspace |
PUT /api/workspaces/{id}/intended-use (the approved intended use & oversight, and the risk level) |
Charts¶
GET /api/charts/help?prefix=<dotted> → "how to read this chart" registry. The
MCP reads it as get_chart_data(family) with kind omitted, marking each entry
fetchable (in the curated allow-list or not); get_chart_data("all") is the
compact index of every family, without the prose.
Any chart endpoint accepts ?format=data to return the raw numbers
({series | rows | header/cells | value, meta}) instead of a Plotly figure —
reason over that JSON, never the {data, layout} figure. See charts.md for the
curated allow-list.
Caps & env knobs¶
| Knob | Default | Effect |
|---|---|---|
PARLEML_AGENT_MAX_ROWS |
1000 |
Row cap for query; body limit can only lower it. |
PARLEML_AGENT_MAX_POINTS |
2000 |
Per-array cap for charts read with ?format=data. |
PARLEML_AUTH_REQUIRED |
false |
When true, all /api/* (except health) and /mcp need a credential. |