External Data Resolver: Compose Two Resolvers in a Single Knowledge Query
One node, two External Data Resolvers, one CIQ read.
Pattern in this example:
1. Define two External Data Resolver configs pointing at the same upstream service but with different response_selector values:
- publisher-brand-resolver: response_selector = ".brand"
- publisher-category-resolver: response_selector = ".category"
Both point at the public test API https://dummyjson.com/products/1 (swap for your own catalog API; we will change the data eventually).
2. Capture a Publisher node where two of its properties carry external_value: { name: <resolver> }. These properties have no value baked into the IKG.
3. The CIQ policy authorizes reads on those properties.
4. The Knowledge Query lists publisher.property.brand and publisher.property.category among the nodes to return.
5. At execute time CIQ invokes both resolvers, selects each response_selector path, and returns the combined record.
Why this matters: external state can be split across small, single-purpose resolver configs without forcing the consumer to know about them - the caller asks for a property by name, CIQ handles the lookup.
Use case
Graph:
Person(alice) -[OWNS]-> Book(book-001) -[PUBLISHED_BY]-> Publisher(wonderland)
Publisher node properties:
- name: "Wonderland Press" (plain IKG value)
- brand: external_value -> publisher-brand-resolver (.brand)
- category: external_value -> publisher-category-resolver (.category)
Single CIQ call returns:
{
book.property.title: "Adventures Underground",
publisher.property.name: "Wonderland Press",
publisher.property.brand: "Essence",
publisher.property.category: "beauty"
}
The UI receives one object and doesn't need to know that two fields came from a remote system. (brand/category are whatever the demo API returns; swap the resolver url + selectors for your real fields.)

Requirements
Prerequisites:
- ServiceAccount credentials: For creating the two External Data Resolver configs, the policy, and the Knowledge Query.
- AppAgent credentials: For ingestion and query execution.
- Bearer token for the Person subject (the subject is a Person, so /contx-iq/v1/execute needs a third-party bearer token).
- The external service: returns JSON containing both ".brand" and ".category" fields. The demo uses https://dummyjson.com/products/1.
Required API access:
- POST /configs/v1/external-data-resolvers (x 2)
- POST /capture/v1/nodes and /capture/v1/relationships
- POST /configs/v1/authorization-policies
- POST /configs/v1/knowledge-queries
- POST /contx-iq/v1/execute
Steps
Step 1: Create the Two External Data Resolver Configurations
- Same url, method, content type. Only response_selector differs (".brand" vs ".category"). Both are referenced from the same Publisher node.
Step 2: Capture the Graph with external_value Properties
- Capture Person, Book, and Publisher nodes. The Publisher's brand and category properties use external_value: { name: <resolver-name> } instead of value: ....
- Capture OWNS (Person -> Book) and PUBLISHED_BY (Book -> Publisher).
Step 3: Create the CIQ Policy
- Subject: Person; filter: subject.external_id = $subject_external_id.
- allowed_reads explicitly lists publisher.property.brand and publisher.property.category so the runtime is allowed to expose the External Data Resolver-sourced values.
Step 4: Create the Knowledge Query
- nodes returns book.property.title, publisher.property.name, publisher.property.brand, publisher.property.category.
- filter selects the book by $book_id.
Step 5: Execute
- input_params.book_id identifies the book. The runtime walks the graph, hits the two External Data Resolvers to resolve the brand/category values, and returns the composed record.
Step 1 (resolver A)
First External Data Resolver: reads the .brand field from the demo upstream (https://dummyjson.com/products/1). response_selector ".brand" picks one field of the upstream JSON.
{
"project_id": "your_project_gid",
"description": "Reads the .brand field from a public test API. One of two resolvers that read different fields of the same upstream response. Swap the url for your own catalog API in production.",
"display_name": "Publisher brand resolver",
"name": "publisher-brand-resolver",
"headers": {},
"method": "GET",
"request_content_type": "JSON",
"request_payload": "",
"response_content_type": "JSON",
"response_selector": ".brand",
"url": "https://dummyjson.com/products/1"
}Step 1 (resolver B)
Second External Data Resolver: same upstream URL, different selector. ".category" picks another field. Splitting these into two configs keeps each resolver's response_selector simple and reusable.
{
"project_id": "your_project_gid",
"description": "Reads the .category field from the same public test API as publisher-brand-resolver — same upstream, different response_selector — so a single CIQ read can surface two externally-resolved fields at once.",
"display_name": "Publisher category resolver",
"name": "publisher-category-resolver",
"headers": {},
"method": "GET",
"request_content_type": "JSON",
"request_payload": "",
"response_content_type": "JSON",
"response_selector": ".category",
"url": "https://dummyjson.com/products/1"
}Step 2
Capture Person, Book, and Publisher nodes. The Publisher's brand and category properties point at the two External Data Resolver configs via external_value.
{
"nodes": [
{
"external_id": "alice",
"is_identity": true,
"type": "Person",
"properties": [
{
"type": "email",
"value": "alice@email.com"
}
]
},
{
"external_id": "wonderland",
"type": "Publisher",
"properties": [
{
"type": "name",
"value": "Wonderland Press"
},
{
"type": "brand",
"external_value": "publisher-brand-resolver"
},
{
"type": "category",
"external_value": "publisher-category-resolver"
}
]
},
{
"external_id": "book-001",
"type": "Book",
"properties": [
{
"type": "title",
"value": "Adventures Underground"
},
{
"type": "isbn",
"value": "978-0-00-000001-1"
}
]
}
]
}Capture OWNS and PUBLISHED_BY relationships.
{
"relationships": [
{
"source": {
"external_id": "alice",
"type": "Person"
},
"target": {
"external_id": "book-001",
"type": "Book"
},
"type": "OWNS"
},
{
"source": {
"external_id": "book-001",
"type": "Book"
},
"target": {
"external_id": "wonderland",
"type": "Publisher"
},
"type": "PUBLISHED_BY"
}
]
}Step 3
CIQ Policy walking Person -[OWNS]-> Book -[PUBLISHED_BY]-> Publisher. allowed_reads explicitly grants read access on the two External Data Resolver-sourced properties.
{
"meta": {
"policy_version": "1.0-ciq"
},
"subject": {
"type": "Person"
},
"condition": {
"cypher": "MATCH (subject:Person)-[:OWNS]->(book:Book)-[:PUBLISHED_BY]->(publisher:Publisher)",
"filter": [
{
"operator": "=",
"attribute": "subject.external_id",
"value": "$subject_external_id"
}
]
},
"allowed_reads": {
"nodes": [
"book.property.title",
"publisher.property.name",
"publisher.property.brand",
"publisher.property.category"
]
}
}Request to create the policy.
{
"project_id": "your_project_gid",
"description": "CIQ policy authorizing a Person to read a Book and its Publisher, including two Publisher properties (brand, category) that are sourced via two separate External Data Resolvers.",
"display_name": "policy - book detail with multi-External Data Resolver publisher",
"name": "policy-extdata-book-detail",
"policy": "{\"meta\":{\"policy_version\":\"1.0-ciq\"},\"subject\":{\"type\":\"Person\"},\"condition\":{\"cypher\":\"MATCH (subject:Person)-[:OWNS]->(book:Book)-[:PUBLISHED_BY]->(publisher:Publisher)\",\"filter\":[{\"operator\":\"=\",\"attribute\":\"subject.external_id\",\"value\":\"$subject_external_id\"}]},\"allowed_reads\":{\"nodes\":[\"book.property.title\",\"publisher.property.name\",\"publisher.property.brand\",\"publisher.property.category\"]}}",
"status": "ACTIVE",
"tags": []
}Step 4
Knowledge Query: one filter ($book_id), four fields returned, two of which trigger External Data Resolver calls.
{
"nodes": [
"book.property.title",
"publisher.property.name",
"publisher.property.brand",
"publisher.property.category"
],
"filter": {
"attribute": "book.external_id",
"operator": "=",
"value": "$book_id"
}
}Request to create the Knowledge Query.
{
"project_id": "your_project_gid",
"description": "Return one book's title plus three Publisher fields. publisher.property.name is plain IKG data; publisher.property.brand and publisher.property.category are resolved at execute time via separate External Data Resolvers (publisher-brand-resolver and publisher-category-resolver) that read different fields of the same upstream response.",
"display_name": "knowledge query - book detail with publisher metadata",
"name": "kq-book-detail-multi-edr",
"policy_id": "your_policy_gid",
"query": "{\"nodes\":[\"book.property.title\",\"publisher.property.name\",\"publisher.property.brand\",\"publisher.property.category\"],\"filter\":{\"attribute\":\"book.external_id\",\"operator\":\"=\",\"value\":\"$book_id\"}}",
"status": "ACTIVE"
}Step 5
Execute the query (input_params: subject_external_id, book_id). The runtime calls both External Data Resolvers as part of resolving the response.
{
"id": "your_query_gid_or_name",
"input_params": {
"subject_external_id": "alice",
"book_id": "book-001"
}
}Response composes IKG-resident publisher.name with External Data Resolver-resident publisher.brand ("Essence") and publisher.category ("beauty") from the demo upstream.
{
"data": [
{
"nodes": {
"book.property.title": "Adventures Underground",
"publisher.property.name": "Wonderland Press",
"publisher.property.brand": "Essence",
"publisher.property.category": "beauty"
}
}
]
}API Endpoints
/configs/v1/external-data-resolvers /capture/v1/nodes /capture/v1/relationships /configs/v1/authorization-policies /configs/v1/knowledge-queries /contx-iq/v1/execute