WL.RSLV.0S1 resolve() · preview {{date}} · Rome · {{clock}}
WordLift / resolve()
// entity resolution API

resolve()

Turn mentions into identities.

Send text. Resolve it against Wikidata, your WordLift knowledge graph or your own entities. Get back a stable identifier for each mention, or unresolved when the evidence isn't sufficient. English, Italian, French, German, Spanish and Portuguese; Japanese and Chinese answer too.

Try resolve() Get a free key Read the docs
Resolver POST /accounts/{id}/resolve
detect → retrieve → resolve
~1 s
curl -X POST "https://api.wordlift.io/accounts/$WL_ACCOUNT_ID/resolve" \
  -H "Authorization: Key $WL_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "{{heroText}}",
       "language": "en"}'
import os, requests

r = requests.post(
    f"https://api.wordlift.io/accounts/{os.environ['WL_ACCOUNT_ID']}/resolve",
    headers={"Authorization": f"Key {os.environ['WL_KEY']}"},
    json={"text": "{{heroText}}", "language": "en"},
)
print(r.json())
import { WordLiftResolver } from "@wordlift/resolve";  // npm i @wordlift/resolve

const wl = new WordLiftResolver({ apiKey: process.env.WL_KEY! });
const res = await wl.resolve({
  text: "{{heroText}}", language: "en",
});
→ 200 OK  // 542 ms, live engine
{"mentions": [
  {"text": "Apple", "start": 0, "end": 5,
   "status": "resolved",
   "entity": {"id": "Q312", "label": "Apple Inc.",
              "types": ["Organization"], "same_as": [...]},
   "score": 0.9977,
   "signals": {"path": "reranked", ...}},
  {"text": "Rome", "start": 28, "end": 32,
   "status": "resolved",
   "entity": {"id": "Q220", "label": "Rome",
              "types": ["City"], ...},
   "score": 0.9982, ...}],
 "dataset_uri": "wikidata://public", "language": "en"}
→ 200 OK  // live engine
{"mentions": [
  {"text": "Apple Street Bakery", "start": 0, "end": 19,
   "status": "unresolved",
   "entity": null,
   "reason": "low_relevance",
   "signals": {"path": "reranked", "match_probability": 0.0, ...}},
  {"text": "Rome", "start": 44, "end": 48,
   "status": "resolved",
   "entity": {"id": "Q220", "label": "Rome",
              "types": ["City"], ...},
   "score": 0.9951, ...}],
 "dataset_uri": "wikidata://public", "language": "en"}
// real responses, 2026-09-27 · long fields trimmed with ... · mentions detected, candidates retrieved by the engineno key? get one
// try it live
resolve.wordlift.io · live enginementions detected by the engine · six languages, plus ja and zh
{{liveStatus}}
{{r.text}} {{r.status}} {{r.entity}} {{r.score}}
// POST /demo/resolve · open, no key · 600 characters · 10 requests a minute per IP · for more, get a key: 1,000 requests a month
See it inside an agent: three live comparisons, with and without resolve() →
Ready to build: get a free key, 1,000 requests a month →
open source + hosted engine
Open where you build. Proprietary where we resolve.

The pipeline, the Resolver interface and the clients are Apache-2.0: extraction, your retrieval, your graph. WordLift runs the decision engine behind POST /accounts/{id}/resolve. You keep control of your content and your candidate universe.

Apache-2.0 pipeline Python · resolve-pipeline npm · @wordlift/resolve OpenAPI API docs llms.txt GitHub →
01 / your data

Resolve against your world

Wikidata is the default, not the limit. Point resolve() at your own entities and every answer is one of your ids, or unresolved.

already on WordLift
Your knowledge graph, from your key

Nothing to upload or configure. Send dataset_uri: "wordlift://dataset/me" and the engine reads your graph with the same key: names, aliases, descriptions, types and sameAs links. Answers carry your IRIs.

your own vocabulary
A JSON list, sent with the request

A product catalogue, a taxonomy, a CRM export. Send up to 5,000 entities as dataset.entities with id, name, aliases, description, types and same_as. Any ids you like come back unchanged.

local entity first
Yours first, Wikidata for the rest

"wordlift://dataset/me,wikidata://public" tries your graph, then Wikidata, per mention. Each answer says which world it came from, and a Wikidata match your graph declares sameAs comes back as your entity with the public ids merged in.

worlds.mapone call, any world
wikidata://public      ─┐  default · 13.6M entities
wordlift://dataset/me  ─┤  your KG, via your key
dataset.entities       ─┤  inline · up to 5,000
mentions[].candidates  ─┘  your shortlist · public
            │
            ▼
        resolve()  ──►  your id  |  unresolved
request · your graph first, Wikidata for the restPOST /accounts/{id}/resolve
{"text": "Our new Rome office opens in May.",
 "dataset_uri": "wordlift://dataset/me,wikidata://public"}

# per mention: your graph answers first;
# otherwise Wikidata; otherwise unresolved.
# each mention reports the world that answered
# in its own "dataset_uri".
request · inline vocabularyPOST /accounts/{id}/resolve
{"text": "ACME Corp opened an office in Rome.",
 "language": "en",
 "dataset": {"entities": [
   {"id": "https://acme.example/kg/acme",
    "name": "Acme", "aliases": ["ACME Corp"],
    "types": ["Organization"],
    "same_as": ["https://www.wikidata.org/entity/Q1319384"]},
   {"id": "https://acme.example/kg/rome-office",
    "name": "Rome", "description": "Acme's office in Rome",
    "types": ["Place"]}]}}
response · your ids, or unresolved200
{"mentions": [
  {"text": "ACME Corp", "start": 0, "end": 9,
   "status": "resolved",
   "dataset_uri": "inline",
   "entity": {"id": "https://acme.example/kg/acme",
              "label": "Acme", "types": ["Organization"],
              "same_as": ["https://www.wikidata.org/entity/Q1319384"]}},
  {"text": "Rome", "start": 30, "end": 34,
   "status": "resolved",
   "dataset_uri": "inline",
   "entity": {"id": "https://acme.example/kg/rome-office",
              "label": "Rome", "types": ["Place"]}}],
 "dataset_uri": "inline", "language": "en"}

Every match still goes through the same context ranking and abstention gate as Wikidata: a name that is not in your data is no_candidates, a name whose context does not fit is low_relevance. The walkthrough with curl, Python, TypeScript and the command line: Run resolve() on your own data.

02 / return NIL

Entity resolution isn't just knowing what to link. It's knowing when not to link.

In production a confident wrong link costs more than no link. It enters the graph, ships in JSON-LD and gets repeated by every system downstream. resolve() is built for precision, calibration and explicit NIL.

[b.64] · hover to decode
evidence.view · mention "Apple"2 contexts · 3 candidates
ctx › {{k.ctx}}
{{r.id}}{{r.label}}
decision {{k.decision}}
support threshold accepted insufficient support illustrative · not released scores
correct link

The graph gains an edge you can trust.

wrong link

A false fact in your graph, markup and agent answers. Hard to find, costly to unwind.

NIL

The mention stays unlinked. It can be reviewed, or linked later when the right entity exists.

03 / evidence

How we evaluate resolve()

Measured against Google Cloud Natural Language on 2026-10-02, on gold sets neither side chose. resolve() answers more mentions and Google is more often right about the ones it answers: we lead in English, German and Spanish and trail in the other five. A stricter decision layer with an explicit NIL is evaluated separately, below, and is not in production. We publish the misses too.

benchmark.report · resolve() and Google Cloud Natural Language · 2026-10-02, long documents 2026-10-09production build
setmentionsresolve()
recall / precision / F1
Google Cloud NL
recall / precision / F1
F1 difference
English · gold set10089.0 / 91.8 / 90.482.0 / 96.5 / 88.6+1.8
English · hard names26670.3 / 82.4 / 75.955.3 / 88.6 / 68.1+7.8
German8693.0 / 96.4 / 94.787.2 / 98.7 / 92.6+2.1
Spanish8386.7 / 93.5 / 90.081.9 / 94.4 / 87.7+2.3
Italian8386.7 / 88.9 / 87.885.5 / 95.9 / 90.4−2.6
French7382.2 / 87.0 / 84.579.5 / 95.1 / 86.6−2.1
Portuguese8490.5 / 96.2 / 93.396.4 / 98.8 / 97.6−4.3
Japanese7970.9 / 84.8 / 77.274.7 / 89.4 / 81.4−4.2
Chinese7859.0 / 82.1 / 68.770.5 / 79.7 / 74.8−6.1
AIDA-B · 230 news articles313873.9 / 87.8 / 80.364.8 / 82.2 / 72.5+7.8
MSNBC · 20 long articles31768.8 / 86.2 / 76.578.2 / 89.9 / 83.6−7.1
Eight gold sets66682.7 / 90.6 / 86.582.4 / 93.8 / 87.8−1.3

Same gold sets and the same matching rule for both systems. Recall is correct over all gold mentions, precision is correct among the links made. Google is the v1 analyzeEntities API, its Wikipedia links mapped to Wikidata ids; it publishes no confidence, so this is its only operating point. Our side is the production pipeline replayed on the same sets. The gold sets are built from Wikipedia and hold 73 to 100 mentions per language, so a difference under about five points is within noise. The two long-document sets are public news corpora (AIDA-B, MSNBC) run end to end on 2026-10-09 with both systems detecting their own mentions; on AIDA-B we lead by 7.8 points on 3,138 mentions, on MSNBC Google leads by 7.1 on 317. Both are on the page because the rule is to publish the misses with the wins.

Hard names: 266 mentions in sentences we wrote to stress short, ambiguous names, such as a surname alone or a city that is also a person. It is a stress test of our own weak spot, not a neutral sample. Google returns no entity for 100 of them and a wrong one for 19; resolve() returns no entity for 39 and a wrong one for 40. The 28 invented company and person names in the same sentences are left unresolved by both. Known limit: a short name with a dominant meaning follows that meaning. In "Jordan scored 45 points against the Knicks" resolve() returns the country and Google returns nothing. Tell resolve() the mention is a Person and it returns Michael Jordan.

The decision layer: a second step that must be able to say NONE. Single frozen runs on held-out sets, 2026-09-23; German replayed on all three of its sets on 2026-09-27. The criteria were fixed before each run: accepted precision ≥ 99%, resolution coverage ≥ 90%, false resolution under forced NIL ≤ 2%. No language has cleared all three yet, so this layer stays on hold and these sets are now regression sets.

gate.report · de (replay 2026-09-27) / it / en · frozen 2026-09-23decision layer on hold
languageaccepted precision
≥ 99%
resolution coverage
≥ 90%
false resolution · forced NIL
≤ 2%
false resolution · natural NIL
reported
verdict
DE · frozen rule, three holdouts replayed129/130 · 99.2%129/145 · 89.0%3/145 · 2.07%0/3hold
IT37/37 · 100%37/37 · 100%3/37 · 8.1%1/2hold
EN41/41 · 100%41/57 · 72%1/57 · 1.8%0/6hold

German row: the rule frozen before the third gate, replayed once on all three held-out sets (145 mentions, 2026-09-27). The three gates themselves, one per set on 2026-09-23, were 100% / 90% / 5.0%, 98% / 88% / 3.5% and 100% / 94% / 2.1%; the rule changed on development data between them, never on a holdout. On 145 mentions the verdict is hold by one false resolution and two links.

Gold present: does adding NONE preserve resolution? Gold removed: does the resolver say NONE instead of the least-wrong candidate? DE and IT fail the second question on closely related identities (a subsidiary for its group, a festival edition for the festival). EN passes it and fails coverage: the strict gate, frozen on Italian development data, rejects correct English choices. German went through two more iterations the same day, each with its own development evidence, frozen rule and fresh holdout. The second gate failed all three criteria. The third, after a structural rule (the primary gate refuses candidates the dictionary does not know under that surface form), passed precision and coverage and missed NIL safety by one case out of 48: the city Essen, removed, was linked to the noun Essen. The criterion stays where it was frozen. All three German holdouts are now regression sets.
candidate recall ≠ resolution accuracy

If the correct entity never reaches resolve(), that is a retrieval problem. If it is present and the wrong identity is selected, that is a resolution problem. We report them separately so every error has an owner.

error.attributionper mention
gold ∈ candidates ?
├── no  → retrieval miss       # candidate recall
└── yes → resolve() returns gold ?
          ├── yes → correct resolution
          └── no  → resolution error  # accuracy
04 / how it works

The decision layer in the middle

resolve() sits after extraction and candidate retrieval. It reasons over the mention, context, candidates and retrieval evidence, then commits or abstains.

pipeline.tracedoc → graph
input
content
articles, pages, product copy
a
extract()
"Roma", "Apple", "Tim Cook"
b
retrieve()
candidates from your KG
c · WordLift engine
resolve()
mention context candidates rank · lexical · priors
disambiguation + NIL detection
wd:Q312
entity id
NIL
abstain
output
graph
KG links, JSON-LD, downstream AI
open-source pipeline WordLift engine
ner()what kind of thing
ner("Il sindaco di Roma ha presentato il piano.")
→ Roma : LOCATION   // a type label
resolve()which thing
resolve("Roma", ctx, candidates)
→ wd:Q220   // Rome, a stable identifier
05 / architecture

Retrieval and resolution are separate on purpose

Your graph defines the world. Retrieval introduces your entities. resolve() decides whether a mention actually refers to them.

engine.specresolve()
separation
retrieval ⟂ resolution

Retrieval systems, vocabularies and knowledge graphs change without retraining a specialized classifier every time the entity universe grows. Point resolve() at your own world: your WordLift knowledge graph, derived from your API key, or a vocabulary sent with the request. Local entity first, Wikidata for the rest, with sameAs links merging the two.

engine
WordLift-owned resolution engine

The decision layer is operated by WordLift. No external classification service in the resolution path, and the models behind it can change without changing the contract.

validation
EN · IT · FR · DE · ES · PT

resolve() answers in six languages today; Japanese and Chinese answer too, unmeasured so far. The NIL-safe decision layer is released one language at a time, each against immutable held-out fixtures and a gate whose criteria are fixed before the run.

knowledge graphs GraphRAG semantic search AI agents content intelligence structured-data pipelines
06 / why

We took the other branch

Entity linkers have mostly been built to always answer: take the top-ranked candidate and move on. resolve() is built to answer when the evidence supports it.

lineage · entity linking approachesbranch: resolve()
string match surface forms NER types, not things entity linking argmax over candidates LLM linkers fluent, overconfident <always answers> resolve() calibrated · NIL-aware <answers when supported> + abstention + retrieval evidence
always answers answers when supported
07 / one mental model

resolve() is the decision layer between detecting an entity mention and committing an identity to the graph.

Graph of Life · entities are born, persist, or return NIL gen {{gen}} · live {{alive}}
new link resolved nil Conway, B3/S23 · toroidal
08 / enterprise

The same API scales to your enterprise graph

Talk to us about enterprise →
private knowledge graphs

Resolve against your WordLift graph, read with your key: your IRIs come back, public ids merged through sameAs.

custom vocabularies

Product catalogues, taxonomies, CRM entities. Your ids in, your ids out.

governed resolution

Explicit unresolved instead of guesses, signals on every answer, per-language gates on frozen held-out sets.

enterprise deployment

Dedicated capacity, volume and latency agreements, onboarding on your own graph.

09 / get started

Start open. Add resolve() when identity matters.

The Content Analysis pipeline is open source. Use it to extract mentions, connect your own candidate retrieval and knowledge graph, inspect the data flow, and build on it.

resolve() adds WordLift's proprietary entity-resolution engine at the decision point: send text, or mentions with your own candidates, and get the supported identity for each, or unresolved.

boundary.mapopen where you build · proprietary where we resolve
open sourceyou run it
extract(document)
        │
        ▼
candidate retrieval  ──────►
        │
        │
your KG / vocabulary
WordLift enginewe run it
► resolve(
     mention,
     context,
     candidates
  )
     │
     ├── entity
     └── NIL
option a · open source
Run the open-source pipeline

Inspect the code, run the pipeline locally, bring your own entities and candidate retrieval, and plug in any resolver. Apache-2.0.

View on GitHub →
option b · WordLift engine
Use the WordLift resolver

Point the pipeline's WordLiftResolver at POST /accounts/{id}/resolve for multilingual entity disambiguation, explicit NIL and managed inference. Same shape, no code change.

Get a free key →
terminalbash
$ git clone https://github.com/wordlift/content-analysis.git
$ cd content-analysis
$ pip install -e ".[ner]"
pipeline.pyresolve_pipeline · Apache-2.0
from resolve_pipeline import run, WordLiftResolver

results = run(
    document,
    retrieve=my_retriever,       # candidates from your KG
    resolver=WordLiftResolver(api_key=WL_KEY),
)
# → per mention: resolved entity, or unresolved
for agents and scripts · the one callllms.txt · openapi.json · contract · your own data
$ curl -X POST "https://api.wordlift.io/accounts/$WL_ACCOUNT_ID/resolve" \
    -H "Authorization: Key $WL_KEY" -H "Content-Type: application/json" \
    -d '{"text": "Apple opened a new store in Rome.", "language": "en",
         "mentions": [{"text": "Apple", "start": 0, "end": 5,
                       "candidates": [{"id": "wd:Q312"}, {"id": "wd:Q89"}]}]}'

{"mentions": [{"text": "Apple", "start": 0, "end": 5, "status": "resolved",
               "entity": {"id": "wd:Q312", "label": "Apple Inc.", "types": ["Organization"],
                          "same_as": ["https://www.wikidata.org/entity/Q312"]},
               "score": 0.99,
               "signals": {"path": "reranked", "retrieval_prior": 0.87, "match_probability": 0.99, "rescued": false}}],
 "dataset_uri": "wikidata://public", "language": "en"}

# status is "resolved" or "unresolved"; unresolved is an answer, never guess.
# score is relevance in [0, 1]; read signals.path before comparing scores across mentions.
# warm requests answer in about a second (engine p50 779 ms, measured 2026-09-27); a graph's first call pays its fetch.
# omit "mentions" to let the engine detect them; omit "candidates" to retrieve against the dataset.
# npm install @wordlift/resolve · pip install "resolve-pipeline @ git+https://github.com/wordlift/content-analysis.git"
10 / get access

Tell us what you're resolving

Four short questions. We read every answer and reply with a key, a plan or a straight "not yet" for your case, usually within a working day.

get a free API key

Free for individual developers: 1,000 requests a month. Long texts count more: one request, plus one per 1,000 characters, so about 160 articles. Four short steps; the key arrives by email.

Thanks. We'll get back to you at {{leadEmail}}.

Meanwhile, the open pipeline and the clients are on GitHub, and the live tester above runs against the same engine.

Step {{leadStep}} of 4
{{leadTitle}}

{{leadHint}}

{{leadKeyHint}} {{leadError}}

We'll use these details to follow up about resolve(). Read our Privacy Policy.

Build with resolve()

Read the docs Try resolve() Get a free key