Skip to content

Improvements through the API

The concept behind improvement analyses is in Insight.

With the routes under /improvements you switch improvement analyses on, read the proposals they write, decide on them and apply them, from your own backend. An administrator does the same in Improvements in the console. The fields of every request and response are in the API reference; this page explains what you do with them.

What you need

  • The deployment runs improvement analyses. Your operator switches them on for the whole deployment (CORE_IMPROVEMENTS_ENABLED); by default they are off. Until then every route here answers 404.
  • The improvements capability in your organisation's ceiling and on your key (Capabilities). With improvements alone you read the settings and the analyses, decide on proposals and start an analysis.
  • management as well to change the settings, because switching analyses on spends the organisation's money. If the organisation above keeps key management to itself, that change gets 403 (Reserved key management).
  • agents, prompts or skills to apply a proposal about an agent, prompt or skill, as that agent's, prompt's or skill's own routes ask too.

An analysis belongs to the organisation, not to one person. A subject token has no access to these routes, and a request with the X-Core-Subject header gets 403.

Every route here is also a tool on your key's MCP endpoint (Administer over MCP).

Switch analyses on and set them up

GET /improvements/settings returns your organisation's settings: whether analyses are on, the clock, the period, every check with its thresholds, the pseudonymisation, the maximum of model calls, when the next analysis runs (next_run_at) and what last month's analyses did (last_month).

Switch analyses on with PATCH /improvements/settings. You send only what changes:

curl -s -X PATCH "$CORE_URL/improvements/settings" \
    -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
    -d '{"enabled": true, "cron": "0 3 * * 1-5", "timezone": "Europe/Amsterdam"}'

Switching on is your organisation's consent: from then on the analysis model reads its conversations, pseudonymised, and every analysis costs model calls that count towards the monthly cap.

Field Default What it says
enabled false whether analyses run
cron, timezone 0 3 * * *, your organisation's time zone when an analysis runs: a five-field cron pattern in an IANA time zone
window_days 7 how many days an analysis reads back, 1 to 30; never further than your organisation keeps traces
checks the six older checks on, the five of Skills, evaluations and leaner agents off per check enabled, threshold, min_turns, sample_size (at most 50) and, for a check that counts people (episode_patterns, shared_procedure, evaluation_drafts), min_people; a check you do not name keeps what it has
trial_key_id null the key of your organisation a trial's runs act as, whose monthly cap is the trials' budget; null starts no trial (Trials)
pseudonymisation the pseudonymisation capability's defaults language, entities and score_threshold: what is replaced. Whether it is replaced is not a choice: always
max_model_calls 40 how many model calls one analysis may make
dismiss_quiet_days 30 how many days a dismissed proposal stays away unless the problem doubles
apply_level every kind propose per kind, how far an analysis applies its proposals itself, with nobody reviewing them: propose, draft or publish; for memory_shared_fact a memory store's own promotion.apply_without_review wins when it says on (publish) or off (propose) (Apply without review)

The checks in checks, with their default threshold:

Check threshold
repeated_first_call 0.8: the share of an agent version's turns that start with the same tool call
missing_tool 0.2: the share of the conversations read in which the same missing tool comes back
failing_skill 1.5: how many times more often turns with the skill fail than the same agent's turns without it
off_purpose_agent 0.25: the share of the conversations read about one topic the instructions do not cover
shared_fact not used: it proposes a fact in people's own memories as a shared memory record, in each memory store whose promotion is on, and how many people must hold it is the store's promotion.min_people (Make a personal fact the organisation's). It reads memory, not traces, so it runs with traces off
unused_skill not used: min_turns (50) is what counts
instruction_procedure 4.0: from how many thousand characters of instructions the check looks for a procedure
shared_procedure not used: min_turns (15) turns of at least two agents and min_people (2) people share a sequence of tools
evaluation_drafts not used: an agent needs min_turns (30) turns, and sample_size (10) conversations are drafted into scenarios
leaner_agent 2.0: from how many thousand characters of instructions a shorter version is tried; min_turns is 100
episode_patterns 0.8: how alike, as cosine similarity, sessions must be to count as one pattern. It reads the episodes, the summaries memory extraction writes of each finished session, of sessions that went clearly well or clearly badly: at least min_turns (3) alike sessions of at least min_people (2, never fewer) people make a lesson, written as a new skill or added to the skill that covers it (A skill learned from sessions). It reads memory, not traces, so it runs with traces off
curl -s -X PATCH "$CORE_URL/improvements/settings" \
    -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
    -d '{"checks": {"off_purpose_agent": {"enabled": false}, "missing_tool": {"threshold": 0.3}}}'

Conversations can only be read while traces are on for your organisation (tracing_enabled, Your own organisation). With traces off, missing_tool, failing_skill and off_purpose_agent calculate the figures only, and repeated_first_call, shared_procedure and evaluation_drafts are skipped altogether and write nothing (skipped_no_tracing). unused_skill, instruction_procedure and leaner_agent read no traces.

The analysis model

GET /improvements/model says which model analyses use now:

{"object": "improvement_model", "model": "claude-sonnet-5", "source": "deployment"}

source is organisation when your organisation chose a model itself, deployment when it follows the deployment's default, and null when there is no model. Without a model missing_tool, failing_skill and off_purpose_agent produce figures only, with no explanation and no proposed change, and shared_fact and episode_patterns propose nothing; repeated_first_call reads without a model and still drafts an instructions_addition. Core never falls back to the helper model (assist_model).

Choose a model with analysis_model on PATCH /organisation, with management and under the same rules as assist_model: a chat model from your organisation's catalogue, not a router. null follows the deployment's default again (Your own organisation).

Read proposals

curl -s "$CORE_URL/improvements/proposals?status=open&check=failing_skill" \
    -H "Authorization: Bearer $CORE_KEY"

GET /improvements/proposals is a cursor-paged list, filtered by status, check, target_kind (agent, prompt, skill, evaluation or memory_store) and target_id. GET /improvements/proposals/{id} returns one proposal with evidence: the turns the finding rests on, at most twenty, newest first. Each piece of evidence is ids only (response_id, thread_id, subject_id) and a time; with response_id you read that turn's trace, under the usual rules for traces. A shared_fact or episode_patterns proposal rests on memory records instead: its evidence has memory_id set and response_id null, and you read the record with GET /memory/{memory_id}.

{
  "object": "improvement_proposal",
  "id": "prop_01M4…",
  "check": "repeated_first_call",
  "kind": "agent_version",
  "status": "open",
  "target_kind": "agent",
  "target_id": "agt_01M2…",
  "target_name": "support-agent",
  "target_version": 3,
  "title": "support-agent starts almost every conversation with get_opening_hours",
  "summary": "…",
  "rationale": "…",
  "figures": {"tool": "get_opening_hours", "share": 0.86, "turns": 412,
              "calls_saved_per_month": 1650, "tokens_saved_per_month": 2900000},
  "change": {"instructions_addition": "…"},
  "has_placeholders": false,
  "evidence": [{"response_id": "resp_01M4…", "memory_id": null, "thread_id": null, "subject_id": "cust-118",
                "observed_at": "2026-10-11T08:14:03Z"}]
}

A key sees only proposals about what it reaches itself: if your key's rules name only certain agents, prompts or skills (Keys, capabilities and policy), it sees no proposal about another one, and such a proposal is 404.

What change holds

kind says what kind of change the proposal carries. change is null when the proposal has figures only, for example because there was no analysis model.

kind change Applying creates
agent_version instructions_addition (text appended to the instructions) or instructions_replacement (the whole new instructions, never both), add_tools (tools of your organisation), add_skills and remove_skills (by id or name) a new draft version of the agent
prompt_version template_addition (text appended to the template) a new draft version of the prompt
agent_create name, description, instructions a new agent, with no tools and no skills, whose first version is a draft
skill_rewrite skill_md: the rewritten SKILL.md a new draft version of the skill, with the skill's files, labels and metadata kept; an agent that pins a version keeps it (Skills through the API)
skill_create skill_md: the SKILL.md of a new skill a new skill whose first version is a draft; no agent loads it until it is published and you apply the agent_version proposal beside it, which an analysis never applies by itself unless you applied and published the skill (A skill learned from sessions)
skill_retire none: the proposal carries figures only (turns, loads, days) nothing; deleting a skill is yours to do, and an analysis never applies it
evaluation_create name, judge_model (null when none qualified; the proposal's figures then say needs_judge) and scenarios, drafted from conversations and rewritten with invented details a new evaluation for the agent, linked to it, whose first version is a draft; no run is queued and no key is named
evaluation_scenarios scenarios, as above the evaluation's next version as a draft, holding its published scenarios and the drafted ones
agent_trimmed model, remove_tools (names of tool and toolset entries) and instructions_replacement, whichever the trial changed publishes the draft the trial measured, as the latest version of the agent (Trials)
connector_request name, description: what the connector should do nothing; someone has to build the connector
memory_shared_fact type, text, metadata (the memory store's promotion.labels to begin with) a shared memory record in the store, for nobody in particular, or the confirmation of the shared record that already says it

A drafted scenario is title, steps (each an input message and its checks) and variables. It names no person, turn or conversation, so erasing a person does not reach it. A skill_create that shared_procedure writes comes with an agent_version for each agent whose turns showed the procedure (A skill learned from sessions works the same way).

Text the model wrote may hold placeholders such as <PERSON_1>, because it only read pseudonymised conversations. has_placeholders is then true. Core never fills them in.

Decide on a proposal

curl -s -X PATCH "$CORE_URL/improvements/proposals/prop_01M4…" \
    -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
    -d '{"status": "accepted", "note": "Picking this up next sprint"}'

status is the new status, note an optional explanation of at most 1,000 characters. The answer is the proposal with decided_by and decided_at. decided_by is the id of the key that decided, or auto when an analysis applied the proposal itself.

From To
open accepted, dismissed, applied
accepted applied, dismissed
dismissed open
applied, resolved nothing further

Only an analysis sets resolved: a later analysis that no longer finds the problem. A dismissed proposal stays away for dismiss_quiet_days days unless the share behind it doubles. applied through PATCH is for a change you made yourself, such as a connector you built; the route below sets it itself.

Core refuses with 409:

  • A proposal cannot move from dismissed to applied. — the move is not in the table.
  • A newer proposal of the same finding is open; act on that one. — you are reopening a proposal whose finding a later analysis has already proposed again.

Apply a proposal

curl -s -i -X POST "$CORE_URL/improvements/proposals/prop_01M4…/apply" \
    -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
    -d '{"change": {"instructions_addition": "Opening hours: Mon-Fri 9:00-17:00."}}'

Applying writes the change through the agent's, prompt's or skill's own route, with your key and so under every check that route makes. Nothing is overwritten: it creates a new version or a new agent, and whoever pins a version keeps it. A new agent, prompt or skill version is a draft, and so is what an evaluation_create or an evaluation_scenarios proposal writes (an evaluation's draft version, which an administrator publishes in the console; a key does not read it). An agent_trimmed proposal is the exception, and publishes at once (Trials). Your keys keep using the published version until an administrator publishes the draft, and applied_ref says "draft": true. An agent version keeps the skill version it pinned, so a new skill version changes an agent only once a new agent version pins it. A request that names the skill itself without a version, in skills, uses the new skill version as soon as it is published. If the rewritten SKILL.md changes the description, retrieval-driven loading no longer picks the published skill for such a request until a version is written through the skill's own route.

The body is optional. Without change, Core applies the drafted change. With change you send a change of the same kind, which takes its place and is checked the same way; that is how you replace a placeholder or rewrite a sentence.

The answer is 201 with the proposal, now applied, with applied_ref, and a Location header naming what was created (/api/v1/memory/{id}). A new agent, prompt or skill version, or a new agent or skill, is a draft your key cannot read: then there is no Location, and applied_ref says "draft": true without the version number, or the id of a new agent or skill. Two applies at once write once; the second gets 409.

Core refuses in this order and then writes nothing. Each refusal is a conflict or another kind, and the sentence in detail says which:

Status detail What you do
404 your key does not reach the proposal
409 A proposal cannot move from … to applied. the proposal is already applied, resolved or dismissed; reopen a dismissed proposal first
409 Core does not apply this proposal: its kind has no change Core can make, or it carries none. Make the change by hand and mark the proposal applied. a connector_request or a proposal with no change: make the change yourself and set the proposal to applied with PATCH
422 the first field of change that is wrong fix that field
409 The change still holds a placeholder such as <PERSON_1>. Send the change with each placeholder replaced. send change with every placeholder replaced
404 your key lacks agents, prompts or skills
409 The target has a newer version than the one this proposal was made against, and applying it would drop what that version changed. The next analysis refreshes the proposal. wait for the next analysis, or make the change yourself on the new version
409 The version this proposal was made against was installed sealed, so a new version cannot be built on words this caller may not read. Write the new version by hand. write the new version yourself

A change that would change nothing, such as a tool the agent already has or a skill it does not load, is 422. After that the agent's, prompt's or skill's route decides as it always does: 403 for a name your key's rules do not name, 404 for a target that is gone or out of reach, 409 for a linked copy, a name already taken or a SKILL.md naming another skill, and 422 for a definition it refuses.

Apply without review

An analysis can apply proposals itself, with nobody reading them, for the kinds your organisation chooses. What that means per kind is in Insight. The choice is apply_level in the settings: per kind, propose (the default), draft (the analysis writes a draft an administrator publishes) or publish (it writes the draft and publishes it). GET /improvements/settings names every kind and its level. agent_version, prompt_version, agent_create, skill_rewrite and skill_create take all three; memory_shared_fact takes propose or publish; connector_request, skill_retire, evaluation_create, evaluation_scenarios and agent_trimmed only propose.

curl -s -X PATCH "$CORE_URL/improvements/settings" \
    -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
    -d '{"apply_level": {"agent_create": "draft", "skill_rewrite": "publish"}}'

A kind you do not name keeps what it has. A level a kind does not take, or a name Core does not know, is 422 on apply_level.<kind> and nothing changes. An analysis never publishes a version that replaces an agent's whole instructions: at publish it waits for you, at draft it is written as a draft. Like the rest of the settings, this needs management.

What an analysis does. After each check it applies every open proposal of a chosen kind that it wrote or refreshed in that analysis. It does so as the organisation, with no key: with the organisation's capabilities and under the whole policy of the organisation and the organisations above it. It is the same apply as the route above, with the same refusals. It never applies a proposal that is accepted or dismissed.

Applied. The proposal is applied, with decided_by "auto" and what was created in applied_ref. The audit trail has an improvements.proposal.applied event with auto: true.

Refused. Nothing was written and the proposal stays open, with the reason in auto_apply_refused. A person or a key then decides on it as on any open proposal; a later analysis that finds the same finding tries it again.

auto_apply_refused What you do
has_placeholders apply the proposal yourself with a change without placeholders
target_changed wait for the next analysis, which refreshes the proposal
policy_denied a rule in your organisation's policy refuses the change; change the rule or decide yourself
capability_off your organisation's ceiling lacks agents, prompts or skills
usage_cap_reached the monthly cap is reached
conflict something with that name already exists, the target is a linked copy, or the SKILL.md names another skill
not_found the target no longer exists
sealed the version the proposal is about is sealed; write the new version yourself
not_applicable the proposal has no drafted change (change is null), for example because there was no analysis model; set an analysis model, or make the change yourself and set the proposal to applied with PATCH

Any other value is the kind of refusal the target's route gave, such as unprocessable_content, or refused when that route named no kind. auto_apply_refused is null when no analysis tried the proposal, or once it is applied.

Counting. An analysis also names, per check in checks, auto_applied and auto_apply_refused: how many proposals it applied itself and how many it left open. Every check that ran has them, 0 when no kind is chosen; they are absent on failed, interrupted and skipped_no_tracing, and on analyses from before this feature.

Undoing. Applying overwrites nothing. You undo a new agent or prompt version by pinning the previous version where it is used (Agents through the API, Prompts through the API); you delete a new agent. A new skill version is a draft, which an administrator discards; once published, it changes no agent as long as no agent version pins it, and a request that names the skill itself goes back by naming the previous version, as name@N.

A skill learned from sessions

When episode_patterns finds a new lesson, it writes two kinds of proposal: a skill_create with the new skill, and an agent_version for each agent whose sessions showed the lesson, with the new skill in add_skills. The agent version needs the skill, so apply the skill_create first; before that, applying the agent version answers 409 (not_applicable). A learned skill is drawn from what people said in their sessions and becomes an instruction on their colleagues' agents, and Core's checks catch names, contact details and ids, not instructions. So even when you switch both kinds to publish in apply_level, an analysis applies the new skill but leaves the agent version open for you, unless you applied the skill yourself and it is published. At draft too, the agent version waits for you. When an existing skill already covers the lesson, you get a skill_rewrite of that skill instead. A learned skill is about your organisation's work: a draft that names a person, or holds an email address, a phone number or a link, is never proposed.

Analyses

The clock in the settings starts analyses by itself. Start one at once with:

curl -s -X POST "$CORE_URL/improvements/analyses" -H "Authorization: Bearer $CORE_KEY"

The answer is 202 with the analysis: status queued, trigger manual. Core refuses with 409:

  • Improvement analyses are off for this organisation; turn them on in its settings.
  • An analysis of this organisation is already queued or running.

GET /improvements/analyses returns the analyses, newest first, cursor-paged. GET /improvements/analyses/{id} returns one, with the period it actually read and, per check in checks, an outcome with its counts, among them auto_applied and auto_apply_refused (Apply without review). An analysis is partial when at least one check could not read everything, and a cancelled analysis says in reason why it did not run.

outcome Meaning
ok the check ran
skipped_no_tracing traces are off; figures only, and nothing at all from repeated_first_call
no_model no analysis model; figures only
pseudonymisation_unavailable the service that replaces data could not be reached; Core read no conversations
cap_exceeded the monthly cap is reached; the rest of the analysis was skipped
budget_exhausted max_model_calls was reached; what was left comes first in the next analysis
window_too_short unused_skill needs a period of at least 14 days
failed, interrupted the next analysis tries again

Beside outcome a check lists counts of what it did and what it left out, each a number and absent when none: for leaner_agent queued, no_judge, no_cost_history, no_trial_key, trial_key_gone, trial_key_uncapped, budget_exhausted, trial_not_permitted, trial_limit, window_too_short, too_few_turns, all_tools_used, no_model_named, names_a_router, no_token_history, current_unpriced, no_cheaper_model, not_shorter, markers_lost, and per trial proposed, not_better, running, target_changed, run_failed and agent_deleted; for the checks that draft text about_a_person, too_close_to_source, not_a_scenario, too_few_kept, too_few_read, too_few_agents, no_answer, no_draft, no_procedure, no_lesson and not_rewritable. Read them as names, not as an error.

An analysis only writes a proposal that your organisation could apply at that moment: before it writes one, Core asks the same questions the apply asks, such as whether your organisation's policy permits the agent's name and whether the capability is switched on, without changing anything. What it leaves out is counted per check in not_permitted, by reason, for example {"policy_denied": 2}. Applying asks again, so a proposal your policy no longer permits is still refused then. When your policy changes back, the next analysis proposes it again.

Skills, evaluations and leaner agents

Five more checks, all off until you switch them on in checks:

Check Looks for Proposes (kind)
unused_skill a skill an agent offers and none of its turns loaded, over at least 14 days agent_version removing the skill; skill_retire for a skill nothing loads
instruction_procedure a procedure in long instructions that a minority of conversations need skill_create and an agent_version with instructions_replacement
shared_procedure a sequence of at least three tools repeated across at least two agents and min_people people skill_rewrite, or skill_create and an agent_version per agent
evaluation_drafts an agent with no evaluation, or one with fewer than 32 scenarios, and conversations of it that went well evaluation_create or evaluation_scenarios
leaner_agent an agent with an evaluation of at least five scenarios and a judge agent_trimmed, once a trial held

A scenario of evaluation_drafts is dropped when it holds a placeholder, a contact detail or an id, or when a step sits too close to the message it came from; that is counted, never left for you to clean up.

Trials

leaner_agent tries one change to an agent, fewer tools first, then a cheaper model, then shorter instructions, as two runs of the agent's own evaluation: the agent's published version and a draft holding the change, over the same scenarios with the same judge. The next analysis compares them. It writes an agent_trimmed proposal only when the candidate's score is at least the baseline's, no step the baseline passed failed, and its cost or its latency is at least 10% lower. Until then the trial is open; at most three start per analysis, one per agent at a time, and an agent is tried once per published version. There is no route to read a trial: you read its result through the proposal and its count in the analysis.

The trial key. Trials run as the key you name in trial_key_id, never as the evaluation's own key. PATCH /improvements/settings takes any key of your organisation (422 on trial_key_id for any other) and null to name none; it needs management as well as improvements, because it chooses whose money trials spend. With no trial key no trial starts (no_trial_key), and a key since deleted, disabled or expired starts none either (trial_key_gone).

Its monthly cap is the budget. Every turn of both runs and every judge call is billed to the trial key, so monthly_cost_cap_eur on the key bounds the trials, inside your organisation's own cap. A key with no cap starts no trial (trial_key_uncapped). Before a trial is queued Core estimates twice what the last finished run of that evaluation on that agent cost, judge included; if that is more than the month has left, nothing is queued (budget_exhausted). The key's policy must reach the agent and the candidate model; if it does not, the trial is not queued (trial_not_permitted).

A trial uses the agent's real tools. A trial run calls the same tools a real conversation calls, so a tool that sends, writes or books does so for real while the trial runs, in both runs. Switch leaner_agent on only for agents whose tools you are content to see called.

The proposal. An agent_trimmed proposal names the change and carries, in figures, both runs (candidate and baseline, each with run_id, score, mean_score, cost_eur and mean_latency_ms), monthly_saving_eur (null when it cannot be estimated), turns_per_month, scenarios, trial_id, draft_version and cannot_show: A trial shows one thing: on these … scenarios, graded by this judge, the leaner agent did not do worse. It cannot show how the agent behaves on topics the evaluation lacks, whether a tool needed once a quarter is still needed, whether the tone drifted in a way the judge does not check, or that the result would repeat: each side is one run. POST .../apply publishes the draft that was measured as the agent's latest version, so every call without a pinned version runs it at once; a draft somebody discarded is written again and published. An analysis only ever proposes it: its apply_level is propose. The proposal's evidence is empty, since it rests on runs.

An agent with no evaluation of at least five scenarios is not tried, and no count names it; the proposals of evaluation_drafts draft one.

Cost and trails

Every model call an analysis makes is a model_tokens row in your usage, with no key, and every analysis that starts is one improvement_analysis row. The runs of a trial are billed to the trial key like any run of that key. What Core sent to the model is kept as a trace of your organisation, readable with your key or in the console. The audit trail records who did what, with events such as improvements.settings.updated, improvements.proposal.updated and improvements.proposal.applied, never text.

Rather than reading the list over and over, follow the improvements.proposal.created and improvements.analysis.finished events (Follow events).

Management routes

Every route here has a twin under /tenants/{tenant_id}/improvements/... for an administrator signed in to the console, with the same requests and answers. A twin sees every proposal of that organisation, and an apply's Location is then under /tenants/{tenant_id}. An administrator sets the analysis model with analysis_model on PATCH /tenants/{id}.