External Data Resolver: Refresh a Node Property at Write Time from an External API
External Data Resolvers can supply property values at WRITE time, not just at read time.
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 to invoke.
4. The runtime calls the External Data Resolver, takes the JSON-selected field, and writes it to the property. The query then reads the same property and returns it.
Key difference from a read-time External Data Resolver (e.g., a captured node whose property has external_value pointing at a resolver): here the property is materialized into the graph on each upsert, with metadata recording which resolver supplied it. The graph carries the audit trail; the source-of-truth stays external.
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. Selects the subject by subject.external_id = $subject_external_id (here, "alice").
2. Walks Person -[OWNS]-> Car -[HAS]-> LicenseNumber to the car identified by $license_number.
3. Upserts car.property.current_value, sourcing the value from 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 came from.
4. Returns car.external_id, car.property.current_value, and the source metadata in the response.
Effect: a single execute call refreshes the value AND returns it. No client-side orchestration between "fetch from external" and "save to IKG" - CIQ handles both as one authorized transaction.

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 identity itself comes from input_params.subject_external_id.
- 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 becomes the value written by the upsert.
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 at execute time.
Step 3: Create the CIQ Policy
- Subject is Person; filter binds subject.external_id to $subject_external_id.
- 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: subject_external_id (alice), license_number (selects the car), value_source (the External Data Resolver config name), source_system (recorded as metadata).
- The runtime invokes the External Data Resolver, writes the resolved value to car.property.current_value, then returns the 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 writes the resolved value onto car.property.current_value at execute time, 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 (filtered by $subject_external_id) 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": "$subject_external_id"
}
]
},
"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\":\"$subject_external_id\"}]},\"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 with external_value referring to the External Data Resolver config (via $value_source), and return the resolved value + 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 refresh its current_value property by invoking the External Data Resolver named in input_params.value_source. The fetched value is written to car.property.current_value with metadata recording which resolver supplied it.",
"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: subject_external_id (alice), 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": {
"subject_external_id": "alice",
"license_number": "KITT 0001",
"value_source": "car-current-value-resolver",
"source_system": "pricing-api"
}
}Response: the freshly-written current_value plus a metadata.source field recording which system supplied it.
{
"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