External Data Resolver: Set a Data Reference in a CIQ Upsert and Return the Resolved Value
A CIQ upsert can attach a data reference to a property, the same way Capture does with external_value, and the read part of the same query resolves it immediately.
Pattern in this example:
1. Define an External Data Resolver config (URL, method, response_selector) pointing at an external pricing API.
2. Author a Knowledge Query whose upsert_nodes section has a property where value is replaced by external_value: "$value_source".
3. At /contx-iq/v1/execute time, input_params.value_source carries the name of the External Data Resolver config the property should point at.
4. The upsert stores that reference on the property (no HTTP call happens during the write). The query then reads the same property, which triggers the resolver, and returns the JSON-selected field.
Key point: the price is never written to the graph. The graph holds the reference plus the metadata you attach (here, the source system), so a later read still fetches the live value from the external service. This is the same read-time resolution as a captured node whose property has external_value; the upsert only sets or changes which resolver the property points at.
Use case
Scenario: A used-car app shows the owner the latest market valuation for their car. Prices live in an external pricing service; the IKG keeps Person -> Car -> LicenseNumber. Whenever the owner opens the car detail screen, the app calls /contx-iq/v1/execute, which:
1. Binds the subject to the caller: subject.external_id = $token.sub, the token's subject (here "alice").
2. Walks Person -[OWNS]-> Car -[HAS]-> LicenseNumber to the car identified by $license_number.
3. Upserts car.property.current_value as a data reference to the External Data Resolver named in $value_source (here, "car-current-value-resolver"). Adds metadata source = $source_system (here, "pricing-api") so a later read can tell where the value comes from.
4. Reads car.external_id and car.property.current_value, which resolves the reference against the pricing service, and returns the value with its source metadata.
Effect: a single execute call points the property at the resolver AND returns the current value. No client-side orchestration between "fetch from external" and "update the IKG" - CIQ handles both as one authorized transaction, and every later read of the property fetches the live price.

Requirements
Prerequisites:
- ServiceAccount credentials: To create the External Data Resolver config, the CIQ policy, and the Knowledge Query.
- AppAgent credentials: For graph ingest and for executing the query (X-IK-ClientKey).
- Bearer token: the subject is a Person, so /contx-iq/v1/execute requires a third-party bearer token alongside X-IK-ClientKey. The subject is the token's sub claim; no subject parameter is sent.
- External pricing API: reachable from the IndyKite runtime. The response must contain the field selected by response_selector.
Required API access:
- POST /configs/v1/external-data-resolvers
- 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 External Data Resolver Configuration
- Define url, method, request/response content type, and response_selector.
- response_selector is a JSON path applied to the upstream response; whatever it selects is the value returned whenever the property is read.
Step 2: Capture the Graph
- Ingest Person (alice), Car, and LicenseNumber nodes plus OWNS and HAS relationships. No current_value at capture time - the upsert will create the property, as a data reference, at execute time.
Step 3: Create the CIQ Policy
- Subject is Person; filter binds subject.external_id to $token.sub.
- allowed_upserts.nodes.existing_nodes includes car so the upsert can target it.
- allowed_reads exposes car, the new property, and the source metadata.
Step 4: Create the Knowledge Query
- nodes returns car.external_id, car.property.current_value, and the source metadata.
- filter selects the right LicenseNumber via $license_number.
- upsert_nodes has one entry referencing the car variable from the policy cypher, with a single property whose external_value is "$value_source" - the External Data Resolver config name is supplied per call - and metadata source = "$source_system".
Step 5: Execute
- input_params: license_number (selects the car), value_source (the External Data Resolver config name), source_system (recorded as metadata). The subject comes from the bearer token.
- The runtime stores the resolver reference on car.property.current_value, then reads the property, which invokes the External Data Resolver, and returns the resolved value and the metadata.
Step 1
External Data Resolver config for the pricing service. The demo url is the public test API https://dummyjson.com/products/1; response_selector ".price" picks the price field out of the JSON response body. Swap the url for your own pricing API.
{
"project_id": "your_project_gid",
"description": "Returns a numeric value (the .price field) from a public test API. The CIQ upsert stores a reference to this resolver on car.property.current_value (external_value); the value itself is fetched from the API each time the property is read, so the IKG reflects the latest value without a stale ingest job. Swap the url for your own pricing API in production.",
"display_name": "Car current-value resolver",
"name": "car-current-value-resolver",
"headers": {},
"method": "GET",
"request_content_type": "JSON",
"request_payload": "",
"response_content_type": "JSON",
"response_selector": ".price",
"url": "https://dummyjson.com/products/1"
}Step 2
Capture the Person (alice), her Cars, and their LicenseNumber nodes. No current_value at capture time - the upsert creates it at execute time.
{
"nodes": [
{
"external_id": "alice",
"is_identity": true,
"type": "Person",
"properties": [
{
"type": "email",
"value": "alice@email.com"
},
{
"type": "name",
"value": "Alice Smith"
}
]
},
{
"external_id": "kitt",
"type": "Car",
"properties": [
{
"type": "model",
"value": "Firebird"
}
]
},
{
"external_id": "caddilacv16",
"type": "Car",
"properties": [
{
"type": "model",
"value": "V16"
}
]
},
{
"external_id": "skodaOctavia",
"type": "Car",
"properties": [
{
"type": "model",
"value": "Octavia"
}
]
},
{
"external_id": "ln-kitt-0001",
"type": "LicenseNumber",
"properties": [
{
"type": "number",
"value": "KITT 0001"
}
]
},
{
"external_id": "ln-cad-007",
"type": "LicenseNumber",
"properties": [
{
"type": "number",
"value": "CADV16-007"
}
]
},
{
"external_id": "ln-oct-2021",
"type": "LicenseNumber",
"properties": [
{
"type": "number",
"value": "OCT-2021-XX"
}
]
}
]
}Capture the relationships: alice OWNS each Car, and each Car HAS its LicenseNumber.
{
"relationships": [
{
"source": {
"external_id": "alice",
"type": "Person"
},
"target": {
"external_id": "kitt",
"type": "Car"
},
"type": "OWNS"
},
{
"source": {
"external_id": "alice",
"type": "Person"
},
"target": {
"external_id": "caddilacv16",
"type": "Car"
},
"type": "OWNS"
},
{
"source": {
"external_id": "alice",
"type": "Person"
},
"target": {
"external_id": "skodaOctavia",
"type": "Car"
},
"type": "OWNS"
},
{
"source": {
"external_id": "kitt",
"type": "Car"
},
"target": {
"external_id": "ln-kitt-0001",
"type": "LicenseNumber"
},
"type": "HAS"
},
{
"source": {
"external_id": "caddilacv16",
"type": "Car"
},
"target": {
"external_id": "ln-cad-007",
"type": "LicenseNumber"
},
"type": "HAS"
},
{
"source": {
"external_id": "skodaOctavia",
"type": "Car"
},
"target": {
"external_id": "ln-oct-2021",
"type": "LicenseNumber"
},
"type": "HAS"
}
]
}Step 3
CIQ Policy: Person subject (bound to the token's sub through $token.sub) owns a Car that has a LicenseNumber. allowed_upserts.nodes.existing_nodes includes car so the upsert clause can write to it. allowed_reads exposes the new property and its source metadata.
{
"meta": {
"policy_version": "1.0-ciq"
},
"subject": {
"type": "Person"
},
"condition": {
"cypher": "MATCH (subject:Person)-[:OWNS]->(car:Car)-[:HAS]->(ln:LicenseNumber)",
"filter": [
{
"operator": "=",
"attribute": "subject.external_id",
"value": "$token.sub"
}
]
},
"allowed_upserts": {
"nodes": {
"existing_nodes": [
"car"
]
}
},
"allowed_reads": {
"nodes": [
"car",
"car.external_id",
"car.property.current_value",
"car.property.current_value.metadata.source"
]
}
}Request to create the policy.
{
"project_id": "your_project_gid",
"description": "CIQ policy authorizing the owner of a car to upsert its current_value property. The value itself is sourced from an external pricing API at execute time via the External Data Resolver config named in input_params.value_source.",
"display_name": "policy - owner can refresh car current_value via External Data Resolver",
"name": "policy-extdata-current-value-write",
"policy": "{\"meta\":{\"policy_version\":\"1.0-ciq\"},\"subject\":{\"type\":\"Person\"},\"condition\":{\"cypher\":\"MATCH (subject:Person)-[:OWNS]->(car:Car)-[:HAS]->(ln:LicenseNumber)\",\"filter\":[{\"operator\":\"=\",\"attribute\":\"subject.external_id\",\"value\":\"$token.sub\"}]},\"allowed_upserts\":{\"nodes\":{\"existing_nodes\":[\"car\"]}},\"allowed_reads\":{\"nodes\":[\"car\",\"car.external_id\",\"car.property.current_value\",\"car.property.current_value.metadata.source\"]}}",
"status": "ACTIVE",
"tags": []
}Step 4
Knowledge Query: select the car via license number, upsert car.property.current_value as a data reference to the External Data Resolver config (via $value_source), and read it back, which resolves the value and returns it with the source metadata.
{
"nodes": [
"car.external_id",
"car.property.current_value",
"car.property.current_value.metadata.source"
],
"filter": {
"attribute": "ln.property.number",
"operator": "=",
"value": "$license_number"
},
"upsert_nodes": [
{
"name": "car",
"properties": [
{
"type": "current_value",
"external_value": "$value_source",
"metadata": [
{
"type": "source",
"value": "$source_system"
}
]
}
]
}
]
}Request to create the Knowledge Query.
{
"project_id": "your_project_gid",
"description": "Find the caller's car by license number, then set car.property.current_value as a data reference to the External Data Resolver named in input_params.value_source, with metadata recording the source system. The IKG stores the reference, not the price; the read part of the same query resolves it and returns the current value.",
"display_name": "knowledge query - refresh car value via External Data Resolver",
"name": "kq-refresh-car-value",
"policy_id": "your_policy_gid",
"query": "{\"nodes\":[\"car.external_id\",\"car.property.current_value\",\"car.property.current_value.metadata.source\"],\"filter\":{\"attribute\":\"ln.property.number\",\"operator\":\"=\",\"value\":\"$license_number\"},\"upsert_nodes\":[{\"name\":\"car\",\"properties\":[{\"type\":\"current_value\",\"external_value\":\"$value_source\",\"metadata\":[{\"type\":\"source\",\"value\":\"$source_system\"}]}]}]}",
"status": "ACTIVE"
}Step 5
Execute the query. input_params: license_number (selects the car), value_source (the External Data Resolver config name to invoke), and source_system (recorded as metadata). Send X-IK-ClientKey, plus a bearer token since the subject is a Person.
{
"id": "your_query_gid_or_name",
"input_params": {
"license_number": "KITT 0001",
"value_source": "car-current-value-resolver",
"source_system": "pricing-api"
}
}Response: current_value resolved at read time from the pricing service, plus the metadata.source field stored on the reference.
{
"data": [
{
"nodes": {
"car.external_id": "kitt",
"car.property.current_value": 9.99,
"car.property.current_value.metadata.source": "pricing-api"
}
}
]
}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