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 answers404. - The
improvementscapability in your organisation's ceiling and on your key (Capabilities). Withimprovementsalone you read the settings and the analyses, decide on proposals and start an analysis. managementas 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 gets403(Reserved key management).agents,promptsorskillsto 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}.