Skip to content

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) and profile
  • 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 with PARLEML_AUTH_REQUIRED=true. Mint under Settings → Access or POST /api/settings/tokens. /api/health is 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) or builder (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 an Idempotency-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: 200 ok · 400 bad request (empty workspace, non-SELECT SQL, unknown table/column) · 401 missing/expired/revoked token when auth required · 403 the token's scope (or workspace binding) does not cover the route · 404 unknown id / experiment not published · 409 it conflicts with the current state (a stale plan, an Idempotency-Key still in use or used for another request) · 413 upload too large · 422 a malformed field · 429 the builder token's run budget is spent. Error body: {code, message, details?}.
  • 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 MCP get_resource adds resource_kind to 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=…) reads GET /api/experiments and returns the experiments still pending or running as job handles ({job_id, kind, status, poll_after_s, ui_url}) to poll with get_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"
    }
  ]
}
  • confidence is an enum — fk | high | medium | low, not a number. Filter join edges with confidence in ('fk','high'); the numeric score, when present, is the separate optional probability.
  • tables[].name is the SQL identifier (use in query). data_type is a DuckDB type string (BIGINT, DOUBLE, VARCHAR, INTEGER, TIMESTAMP_NS, BOOLEAN, …).
  • dataset_id is the join to results-catalog. is_result separates published results (true) from input datasets (false).
  • describe_catalog(workspace_id, preview="<object>") returns the first 20 rows of one object through GET /api/knowledgebases/{kb_id}/object-preview?object=…&n=20 — a lakehouse table by default, or with knowledgebase_id a 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"
      }
    }
  ]
}
  • table is the canonical name — never used in SQL (canonical_id in the MCP result).
  • columns[].dtype is the logical type (int | float | str | bool), distinct from the DuckDB data_type reported by data-kb.
  • foreign_keys[].references_table is the canonical name of the target table; the MCP result leaves it canonical, so match it to a canonical_id and use that table's name.

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.
  • limit only lowers the server cap (agent_max_rows, default 1000); user LIMIT/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 cached EDAResult (table stats, per-column variables, 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 → the LeakageReport (target-leakage scan; 404 until prepare_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 packed mt list) or free text — are listed under role_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). A single_value finding (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 under hygiene, 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, with carried_from naming 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 answers 202 {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 tool await_operation, then read the result with get_eda. Other work on a dataset whose EDA is running is a 409 naming the same handle. One that runs far longer than it should is stopped with prepare_data(dataset_id, action="cancel") (POST …/eda/cancel, the cancel_route); one that makes no progress at all is failed by the server on its own after PARLEML_JOBS_STALL_AFTER_S (15 min): its waiters get 503 job_stalled.
  • A recipe materialise (author_recipe(action="materialize" | "refresh")) scans the new dataset for leakage and answers leakage_high_severity; when that scan did not finish (failed, stalled or stopped) it is null and leakage_scan_error says why — scan again with prepare_data(action="leakage") before planning.
  • GET /api/datasets/{id}/columns/evidence → ColumnSuggestionsResponse without 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) and prose_rate — plus the current overrides, and the suggestions the checks support (source: "checks"). The Columns tab's AI-written suggestions are the dataset's columns.describe annotation, 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 as target_col), task? (auto, binary_classification, multiclass_classification or regression). 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_total says how many there are). Cached per target/params; refresh=true recomputes.
  • 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 answers 202 {status: "queued" | "running", wait_handle: "probe:<kind>/<dataset_id>/<digest>", poll_after_s, queue} (queue while it waits for a slot: why, and how far back). In a session ParleML runs, the session tool await_operation (operation_id = the wait_handle) holds until the result is cached (then the same call returns it at once); elsewhere, repeat the same call after poll_after_s.
  • forecast_signal → POST /forecast-signal-check. Params: time_col, target_col, series_id_col?, horizon?, resample_to?, resample_agg? (server default sum). Returns the verdict, the zero-shot Chronos vs SeasonalNaive MASE and skill, the backtest curves, and grid. Cached; refresh=true recomputes.
  • baseline_floor → POST /baseline-floor. Params: target, task (one of the three tasks — no auto, because the floor depends on it), metric (an EvalMetric name, sent as eval_metric), positive_class? (binary only). Returns strategy (the strongest feature-blind predictor) and its score in 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 temporal experiment.validation block): time_column, temporal_boundary_mode? (fraction, the default, or date), holdout_frac? (the test share, default 0.2), outer_test_cutoff? (required in date mode: the test starts after it), validation_cutoff? (date mode only, optional: where the calibration period starts — without it, the last validation_holdout_frac of the rows before the test), validation_holdout_frac? (config.engine.validation_holdout_frac, default 0.1) and embargo? (an integer + H, D, W or min, 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, and launch — validation to paste into the plan's experiment.validation and validation_holdout_frac into config.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}/predict body { "row": {…}, "calibrated": true } → composed row scoring (prediction + drivers + archetype + neighbours) for ONE raw row — the what_if tool on a supervised run. With explain=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/predict body { "rows": [ {…} ], "include_reasons": true } scores the row and gives its reason codes. The MCP stamps scoring (supervised, supervised_local or unsupervised) on the result.
  • "What if this case's X were Y": every one of those bodies takes base — {"row_index": n} (the row_index of the run's published tables) or {"key": {"claim_id": "…"}} (the dataset's declared key; exactly one row must match) — and then row holds only the columns that change ({} scores the case as it is). Without base, row is 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 by base. The answer's row_basis (a list of them for the unsupervised rows) says what was scored: base_row_index, replaced (the base row's own values of the columns row set), nulled_columns (sent as missing) and ignored_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-threshold confusion_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/*, /ranking and /ltr/* summaries. The MCP get_insight tool wraps the curated set as named kinds: get_insight(id, "kinds") lists the ones that apply to an experiment; their meanings are in explainability.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), /notifications and /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 a next hint.
  • stages / pipeline — /stages / /pipeline: the stage list; the stage graph.
  • events — /events?since_seq=N: the last limit events (default 50); next_seq is the cursor to pass back as since, and restarted says the event log started over.
  • log — /log?tail=N: the console's last limit lines (default 80), with earlier_output when more exists. Read it when a run failed.
  • notifications — /notifications: the warnings and notices the run raised (the last limit, default 100).
  • settings / config — /run-settings: the labelled launch settings; or resolved_config (the config the run used) with resolved_provenance.
  • explanations — /mli/status and /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/compare body { "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, unavailable reasons), metrics (one entry per metric × split: values aligned with rows, the best row 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 says is_production_model) and notes. Read-shaped: any analyst token. 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 in unavailable (explain it again).
  • POST /api/models/compare body { "ids": [2–4 registered model ids] } → lineage and headline scores, in request order. Read-shaped. MCP: compare("models", ids).
  • POST /api/datasets/compare body { "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_features is the full count; the panel gets them all). GET /api/datasets/compare/feature/{feature}?reference_id=…&current_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_resource for a dataset, experiment or knowledge base, get_run(view="explanations") (explain.narration, explain.fields, explain.rule, explain.class, clusters.names) and describe_catalog for a knowledge base (kb.describe, kb.relationships, recipe.explain) add annotations: {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; accepted lists the items already taken.
  • Agents do not ask ParleML to write them. POST /api/annotations/… and GET /api/annotations/status refuse 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 through prepare_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.