How healing works
What Kaabist does when an element your program points at moves, how confident it has to be, and why a person approves every change to a program.
When a site is redesigned and a locator stops matching, Kaabist searches the failed page for the element your program meant, scores how sure it is, and files the change as a proposal. A confident match can carry the running job to the end, but only a person approving the proposal changes your program.
#When to use it
Healing is always on; there is nothing to enable. This page matters when a job parks with the reason drift, when a result carries a healed_locator_used or fill_rate_drop warning, or when the review queue has relocate proposals waiting for you. Read it before you approve one, so you know what the confidence number measures and what approving with and without promote does.
Healing does not cover a page that is wrong rather than moved: a login page instead of a dashboard is unknown_page or session_expired, and an overlay in the way is an obstacle.
#The idea
Sites get redesigned. A price that lived in div.price ends up in span[data-col="amount"] behind two new wrappers, a product card stops being an <li> and becomes an <article>, a build tool rehashes every class name in the stylesheet. None of that changes what the page says, and that is the basis of healing: the values survive the redesign, so the values are how the element is found again.
Healing never runs on a job that succeeded. It runs on the failure path, and on the statistics that notice a field has stopped filling before any job fails.
#The reference it heals against
Kaabist remembers what "correct" looked like. When a program version is created from the builder, or a field is confirmed in the picker, and on the first successful run of a program version, it stores for every field and every step target: a fingerprint of the element that matched (its tag, its identity-bearing attributes, its text, five ancestors, where it sat among its siblings), the raw string it printed and the value your parser made of it, and the page itself. That bundle is the ground truth every later search is measured against. A later run never overwrites it, because a reference that moves with the page is not a reference.
The same first successful run adds more stable candidate locators to any target whose stored list has fewer than three, so a redesign that breaks the first candidate often leaves another standing. See Locators.
#Where healing runs
There are two paths, and which one applies depends on what broke.
- A step's target (the element a
click,fill,hoveror similar acts on) that stops resolving is healed inside the job, against the page captured at the moment of failure, within half a second. - An extract field that stops filling does not fail the job: it comes back
nullwith a warning. Kaabist watches the fill rate of every field of a published version against its trailing seven-day baseline; when it drops (by 30 points by default), or when a version's canary fails, the version is flagged, every job of that version carries afill_rate_dropwarning, and the healer service looks for the relocation and files it as a proposal before you ask.
The healer service also takes over when an inline heal was not enough and your organisation has LLM credits enabled: it looks at the parked job again with a larger time budget and a model.
#How the search works
Four deterministic stages run over the page that failed:
- The locators you already have. A target stores an ordered candidate list. A redesign that broke the first two may have left the third standing.
- Tree comparison. Every element of a plausible tag is scored against the stored fingerprint: how alike the paths, attributes, text and neighbours are.
- Value-shape anchors. For a typed field (money, a number, a date), Kaabist looks for text that parses to the same type and agrees about magnitude: the same currency, within a factor of ten, within a year. For a string, a Levenshtein ratio of 0.8 or better. A cell under a heading the old page also knew scores higher than one that stands alone.
- Lists. A repeating structure is healed as a structure: the item pattern is found by what repeats around a candidate, and how consistently the list's own fields resolve across its items is part of the score.
An optional fifth stage asks a language model, only if your organisation has enabled LLM credits. It is never trusted: it may reorder candidates the deterministic stages already found, or suggest selector strings, and every selector it writes is resolved against the page before it counts for anything. A selector that matches nothing, or matches four hundred things, is discarded.
With Kaabist's model, each call is charged to your organisation as model tokens, at the model's rates, and shows in the ledger against the job it healed. The model is skipped, and the heal stays deterministic, when your balance could not cover a call. Your own model, and jobs run with a test key, are not charged.
#Confidence
Every candidate gets a number on [0, 1], a weighted mix of five terms:
| Weight | Term | What it measures |
|---|---|---|
| 35 % | structural similarity | how alike the new element's fingerprint is to the stored one (1 for a stored candidate that resolved) |
| 30 % | value-shape agreement | whether what the new element prints agrees with the stored sample: 1 when it parses to the field's type and its magnitude agrees (a number within 10×, a list within 50 % of its length, a string at a 0.8 ratio, money in the same currency, a date within a year); 0.5 when it only parses to the type; 0 when it does not parse |
| 15 % | uniqueness | 1 when the new locator matches one element, 1/n when it matches n |
| 10 % | stability rank | how durable the new locator is: a test id ranks above a role, a role above a class, a class above a position |
| 10 % | list consistency | inside a list, the fraction of items in which the field still resolves; 1 outside a list |
Parsing in the value-shape term uses the field's own configuration, including its locale, so "does this read as money" means exactly what it means when the field is extracted. A part number such as SKU: HP-1037 does not count as a price, even though it contains a number of the right size.
#What happens at each level
| Confidence | In the job | In the review queue |
|---|---|---|
| 0.90 or more | the new locator is used for the rest of this job, and the result carries a healed_locator_used warning | a relocate proposal is filed |
| 0.50 to 0.90 | the job parks as needs_attention with the reason drift, the proposal attached to attention.proposals | a relocate proposal is filed |
| below 0.50 | the job parks as needs_attention: drift with the evidence it has | nothing is filed: a guess that weak is not worth a reviewer's minute |
No confidence promotes anything. A relocation at 0.90 or above changes one run: your program is unchanged, and the next job starts from the same broken locator until somebody approves the proposal. A machine that could silently rewrite what your program means would be one you could not audit.
#Proposals
A proposal is a machine-generated change waiting for a person. Healing files relocate proposals; the same review queue holds every other kind.
kind | What it proposes |
|---|---|
relocate | new candidate locators for a field or a step target, from the healer |
dismiss | a way to close an overlay on this site (see Obstacles), from the obstacle layer or a takeover |
insert_steps | steps recorded while a person took over a parked job |
parser_config | a change to how a field is parsed, such as its locale |
api_discovery | a JSON endpoint the page called, which a field could read instead of the markup |
source says what produced it: heuristic (the deterministic stages), llm, takeover or canary. status is pending until somebody decides, then approved or rejected; a proposal nobody decided becomes expired.
#What a proposal shows you
Enough to answer one question without opening the site: is this the same element?
payload key | |
|---|---|
old / new | the candidate locators, before and after |
samples | the raw strings each one read, and what the field's parser made of them |
fingerprints | the stored element's description, the new one's, and how alike they are |
bboxes | where each element sat, so a screenshot can be read against them |
screenshots | the page when the ground truth was taken, and the page that failed |
evidence | one entry per stage that contributed, with the terms of the confidence |
evidence keeps the runners-up as well, so "this element scored 0.91 and the next one 0.88" is visible. Which keys are filled depends on what produced the proposal; a client should tolerate any of them being absent.
#Deciding
GET /v1/proposals?status=pending&program=shop-price&kind=relocate lists what is waiting. Then:
POST /v1/proposals/prp_01J…/approve
{ "promote": true, "notes": "price moved into the new amount column" }
| Decision | Effect | Scope |
|---|---|---|
| approve, neither flag | the change applies to the parked job waiting on it | one job |
approve with promote: true | a new published version of the program carries the change; the alias latest moves to it, production does not | the program |
approve with remember: "site" | a dismiss becomes a recipe on your site profile, used by every program of your organisation on that hostname | the hostname |
reject, with an optional reason | nothing changes; the decision is recorded | none |
POST /v1/proposals/{id}/edit with { "new": { … } } replaces the proposed change before anybody approves it. It is accepted for relocate and parser_config, the two kinds whose suggestion is a value a reviewer can sensibly correct. The edit is applied to the program and linted before it is stored, so an edit that would break the program is refused at once rather than at approval.
A job parked needs_attention carries its proposals inline, and POST /v1/jobs/{id}/resume with { "apply": "prp_…", "promote": true } decides and resumes in one call. Without promote, the change applies to that run and nothing else.
Every approval is recorded in the audit log with who decided: a person, or the API key their automation used.
#Who may approve
Deciding a proposal needs the owner, admin, developer or reviewer role in the panel, or an API key with the run scope. A viewer can read proposals but not decide them. A coding agent connected over MCP needs the kaab:approve scope. See Accounts and roles.
#Expiry
A proposal attached to a parked job expires with the park, seven days after the job parked. A proposal the healer service filed from statistics or a canary waits thirty days. When a person takes over a parked job and the recording shows what actually works, the machine's pending guess for the same target is retired as expired, not rejected, because nobody decided it was wrong.
#Worked example
A shop ships a redesign that moves every element and keeps every value: before it, a product page prints its price as <div class="price">1.337,40 TL</div>; after it, the same amount sits in .price-wrap > .price-inner > span[data-col=amount], and the price class is gone.
- A program written against the old design hovers the price before reading the page:
{ "kaab": "1", "options": { "max_tier": "browser", "prefer_tier": "browser" }, "steps": [ { "id": "open", "goto": "https://shop.example.com/p/1" }, { "id": "reach-price", "hover": ".price" }, { "id": "read", "extract": { "title": "h1", "sku": { "selector": ".sku", "regex": "SKU:\\s*([A-Z0-9-]+)" } } } ] }
Published asshop-price, its first job succeeds, and that run stores the ground truth for.price: the element, its text1.337,40 TL, and the page. - The shop ships the redesign. The next job's
reach-pricestep finds nothing for.price. Healing searches the failed page: tree comparison finds the newspan, and its text parses to the same amount in the same currency. Depending on the score, the job either finishes with ahealed_locator_usedwarning onsteps[1], or parks withstatus: "needs_attention"andreason: "drift". Either way arelocateproposal targetingsteps[1]is in the queue, withsamplesshowing1.337,40 TLon both sides. - A reviewer opens it under Review in the panel (described below) or lists it with the API, and approves it with
promote: true. The program now has version 2, published, with the new candidates for that step, andlatestpoints at it. - The next job runs version 2 and succeeds with no
healed_locator_usedwarning: the program itself has changed, not one run.
// What healing found and is waiting on a person: the old and new locator, and how sure it is.
let pending = 0;
for await (const proposal of kaab.proposals.list({ status: "pending", limit: 25 })) {
pending++;
console.log(
`${proposal.id} ${proposal.kind} ${proposal.target} confidence ${proposal.confidence} (${proposal.source})`,
);
// Then: kaab.proposals.approve(proposal.id, { promote: true }) or .reject(id, { reason })
if (pending >= 25) break;
}
console.log(`${pending} pending proposal(s)`);
#In the panel
Review lists proposals with filters for status, kind, source and program. Opening one shows its kind and target as the title and the program version it was proposed for. The page has sections for the page itself (the old and new screenshots side by side, with the element highlighted on each), the locators (old and new candidates with their stability ranks), the samples (the raw string and the parsed value before and after), the proposed change, and the evidence per stage. Approve opens a confirmation with a Promote switch ("publish a new version of the program with this change; the alias latest moves to it") and, for a dismissal, Remember for this site, plus optional notes. Reject asks for an optional reason. A decided proposal shows who decided, with which flags, and the version it was published as.
#Pitfalls
- Approving without
promotefixes one job. The next job fails the same way. Promote when the change is right for the program, not only for the run in front of you. promotemoveslatest, notproduction. Jobs and schedules that runmy-program@productionkeep the old version until you move the alias. That is deliberate: check the new version, then moveproduction.- A program with no successful run cannot be healed. There is no ground truth to compare against, so the job parks with a detail that says so. Run a new program once against a page that works.
- A field with no
sampleheals with less evidence. The value-shape term needs to know what the field used to print. The picker writessamplefor you; add one by hand to a field you wrote yourself. evaluatesteps are opaque to healing. Kaabist cannot see what a script did with the page, so it cannot heal around it.healed_locator_usedis not a success to ignore. It means the run was carried by a guess. Review the proposal the same day.
#Reference
- Approve a proposal, reject, edit, list
- Resume a job
- Proposal schema
- Attention reasons and warning codes
- The
proposal.createdandproposal.decidedevents kaab proposals approve- Drift detection and canaries