Distill
Turn a document, a diff, or a post-mortem into durable learnings and facts, when there is no run trace to observe.
In one line
observe is for what your agent did. distill is for what your agent read.
Most of what an agent learns comes out of finished work, through the learning loop. But plenty of durable knowledge never passes through a run at all. A post-mortem explains why last quarter's migration went wrong. An RFC settles a convention the whole team now follows. A diff shows the shape of a fix. None of that is an execution trace, and submitting it as one means inventing tool steps that never happened, which corrupts the very evidence the corpus is built on.
POST /distill is the door for that material. It takes a corpus of evidence and extracts the same kind of learnings and claims the automatic loop produces, without pretending the corpus was a run.
When to use it
| You have | Use |
|---|---|
| A finished run your agent actually performed | observe |
| A document, diff, RFC, or post-mortem worth learning from | distill |
| The final rule text already written, in your own words | Store a learning |
The middle row is the one people miss. The last one is worth stating too: a rule you already have, sent here, pays for an extraction that can only rediscover what you typed.
Submit a corpus
curl -X POST "https://api.hyperstruck.com/distill" \
-H "Authorization: Bearer <API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"agent_name": "support-agent",
"run_id": "distill:checkout-postmortem-2026-07",
"goal": "What the checkout outage taught us about retry configuration.",
"outcome": { "is_success": true },
"evidence": [
{
"id": "postmortem",
"content": "The retry budget was shared across all three downstream calls...",
"role": "contrast",
"label": "incident",
"subject": "checkout-service",
"declared_sensitivity": {
"provenance": {
"source_id": "wiki",
"source_class": "document",
"source_time": "2026-07-19T09:00:00Z",
"author": "sre-oncall"
}
}
}
]
}'Required scope: agents:write. Returns 202 Accepted once the request validates; extraction happens in the background.
Check is_duplicate on that response. run_id is your idempotency key, so a second submission under a run_id already used is accepted and then does nothing at all. It is still a 202, and without reading the flag a resend of a corrected corpus looks like it worked and produced silence.
agent_name is a name, not a UUID
distill is not scoped under /agents/{agent_id}. Its body carries agent_name, and it must be the same name you use on resolve and observe if you want everything landing in one corpus. Passing the hosted agent UUID here does not fail; it files the work under a different agent whose name happens to be a UUID. See what the name is and when it creates an agent.
Fields
Prop
Type
That is the whole vocabulary. The request model rejects any field it does not recognise, so an unlisted key is a 422 rather than something quietly ignored.
The evidence item
One item is one thing you are handing over: a single document, one message, one row. Only id and content are required, but the three optional fields below decide whether the facts inside it are usable.
Prop
Type
Declaring where an item came from
A fact is only as useful as its source. The provenance record inside declared_sensitivity is how an item says where it came from, and it is the difference between a fact the runtime can act on and one it holds at arm's length.
"declared_sensitivity": {
"provenance": {
"source_id": "salesforce",
"source_class": "crm",
"source_time": "2026-08-17T06:00:00Z",
"author": "nightly-sync"
}
}Prop
Type
Those four keys are the whole vocabulary. Anything else in the record is dropped, as is any value over 200 characters, because the record is stored verbatim and rendered into a prompt.
An item with no provenance yields no facts
Declaring provenance is what makes an evidence item a source of claims. Without it the item still feeds learning extraction, but the facts inside it are not written, and nothing errors: the job returns 202 and the corpus is simply thinner than you expect. If a corpus lands and no facts appear, check this first.
A declared source is a statement about where content came from, never a claim that it is trustworthy. Facts from a source nobody has vouched for are held for review rather than used, and a space steward vouches for a source once to change that for everything bearing it.
Pre-redact your secrets
Evidence is text you chose to send. Credentials are blocked before anything is stored or indexed, but the cheapest place to remove a key from a diff is before it leaves your process.
A corpus without contrast is accepted, not refused
Contrast is what the learning extractor compares against, so a corpus that declares none has less for it to work with. That used to be a 400. It is not any more, because the same corpus can be full of facts, and refusing it at the door refused the claim shelf along with the rule shelf.
So the guidance now arrives after the job rather than instead of it. Poll GET /learning-runs/{run_id}/status with the run_id you submitted.
| Field | What it tells you |
|---|---|
state | queued, running, or finished. |
learning_count, claim_count | What the job wrote to each shelf. Null while the run is unfinished, which is not the same as zero. |
zero_reason | Why a count came back zero. Set only when one did. |
corpus_items_lost | Items in a pass that failed, so they were never read. Non-zero means resending is worthwhile. |
corpus_items_with_claims | Items that produced at least one fact. |
zero_reason is a closed set, so you can branch on it: weak_contrast, nothing_extracted, dedup_absorbed, no_entity_resolvable, gate_rejected, failed. The first two are the pair worth telling apart. weak_contrast means the corpus declared no contrast, so add some. nothing_extracted means it declared contrast its text does not actually carry, so the corpus itself held nothing reusable. An earlier reason can preempt weak_contrast, so read the value you get rather than assuming which one applies.
A zero-yield job still costs
The spend reservation settles as used, not released. The flag is about the work done rather than what the work yielded, and a pass that ran a model call and stored nothing has spent the money the cap exists to see. Resubmitting a contrast-free corpus in a loop consumes budget every pass. Read zero_reason, fix the corpus, then resend under a new run_id; the old one is already claimed and would be accepted as a duplicate that does nothing.
What comes back
Extracted learnings are searchable through GET /agents/{agent_id}/learnings/search once the status endpoint reports finished, alongside everything the automatic loop earned. There is no separate shelf for distilled content: a rule earned from a post-mortem sits in the same corpus as a rule earned from a run, and earns its place on the same evidence and outcome machinery. A distillation is stamped as its own kind of job, so loop-closure reporting can tell the two apart without the corpus having to.
Facts extracted from the same corpus land on the claim shelf under each evidence item's subject, or under the entity read from its prose when none was declared, and are governed exactly like any other claim, including quarantine, corroboration, and erasure.
Related pages
- Learning loop for the resolve and observe pairing this sits beside
- Learning for what a learning is and how it earns its place
- Claims for the fact shelf a distilled corpus also writes to
- Learnings API for searching, storing, and reinforcing by hand
- MCP server for driving the same operation as a tool from an IDE