IndyKite ContX IQ - add a property to an existing relationship
Set or overwrite one or more properties on a relationship that already exists in the IndyKite Graph (IKG), driven by a ContX IQ policy + Knowledge Query and run via POST /contx-iq/v1/execute. The policy whitelists which cypher-matched relationships may be modified (allowed_upserts.relationships.existing_relationships); the Knowledge Query's upsert_relationships references those variables (no source/target/type, since the relationship already exists) and lists the properties to set, optionally with metadata. Adding a brand-new property and overwriting an existing one are the same operation - the platform doesn't distinguish.
This skill is the relationship counterpart to indykite-ciq-add-property, which sets properties on existing nodes. The structure is symmetric; the field names are different.
Other paths are deliberately out of scope:
- Creating a brand-new relationship uses
allowed_upserts.relationships.relationship_typesand a Knowledge Queryupsert_relationshipsentry with a freshname+source/target/type- seeindykite-ciq-create-relationship. - Setting properties on a node uses
allowed_upserts.nodes.existing_nodes- seeindykite-ciq-add-property. - Deleting a property on a relationship uses
allowed_deletes.relationshipswith a<var>.<property>path - seeindykite-ciq-delete.
For reads, see indykite-ciq-read.
When to use
Activate this skill when the user:
- wants to set a property on a relationship that already exists in the IKG (e.g. add
verified: trueto an existing:PLAYED_AT, setweighton an existing:LIKES, attachconfidencemetadata to an existing:OWNSedge); - is annotating an existing relationship with provenance, trust score, or audit fields after the fact;
- is debugging a
403/422from a relationship-property-write execute that should have succeeded.
Do not activate this skill when the user:
- wants to create a new relationship between two existing nodes - use
indykite-ciq-create-relationship; - wants to set properties on a node - use
indykite-ciq-add-property; - wants to delete a property - use
indykite-ciq-delete; - wants to read data - use
indykite-ciq-read; - is using the Capture API to ingest data - different ingestion path.
Prerequisites
- An IndyKite project, AppAgent, and AppAgent credentials (the AppAgent token goes into
X-IK-ClientKeyat execute time). - A Service Account token with Config API access, and the project's GID in
PROJECT_GID- both used to create the policy and Knowledge Query. - The target relationship already in the IKG, plus both endpoint nodes.
- A clear list of property names the policy/KQ will write. Property names must be hardcoded in the KQ; only values and metadata may be
$param. - For non-
_Applicationsubjects, the subject's node also already in the IKG.
If any of these are missing, stop and tell the user - fixing them first is much cheaper than debugging a vague 403 or empty result.
Steps
1. Pick the subject and the cypher anchor
Subject type - pick one. The schema is identical across both choices; only subject.type, the filter, and the execute-time auth differ:
| Subject | Use when | Auth at execute time | Filter convention |
|---|---|---|---|
_Application |
System-side / ETL / catalog work; no user in the loop. | X-IK-ClientKey only. |
subject.external_id = $_appId (reserved). |
Person / User |
The authenticated user is performing the operation themselves. | X-IK-ClientKey + Authorization: Bearer <token>. |
subject.external_id = $token.sub. |
A policy is restricted to a single subject type - if both should be allowed, write two policies. The runnable example below uses _Application (system-side annotation pass on existing edges); a Person variant - for example, a user marking their own :LIKES edge as priority - differs only in subject.type, the filter, and the execute headers.
Cypher pattern - must MATCH the existing relationship and bind it to a variable. The variable name is what existing_relationships and upsert_relationships[].name reference. Pin both endpoints by external_id in the filter so the relationship is uniquely identified. If the exact node types, relationship types, or property spellings in the project's IKG are unknown, read them from the Data Schema API first (indykite-data-schema) - a typoed name silently matches nothing, and a write whose pattern matches nothing is a no-op that still returns 200.
Working example (used throughout this skill):
An
_Applicationannotates an existing(:Track)-[:PLAYED_AT]->(:Venue)relationship by setting averifiedflag and afirst_played_attimestamp.
MATCH (subject:_Application)
MATCH (track:Track)-[r:PLAYED_AT]->(venue:Venue)
Variables: subject, track, r, venue. The relationship variable r is the one we're updating.
2. Author the policy with allowed_upserts.relationships.existing_relationships
Build the policy JSON with four blocks:
meta.policy_version- currently1.0-ciq.subject.type-_Applicationfor the running example.condition.cypherandcondition.filter- the cypher matches the existing relationship; the filter pinssubject.external_id = $_appId(reserved) plus the source and target endpoints byexternal_id.allowed_upserts.relationships.existing_relationships- array of relationship variables fromcypherwhose properties the Knowledge Query may write. The Knowledge Query'supsert_relationships[].namemust be in this list.
Omit allowed_reads, allowed_deletes, and the other allowed_upserts sub-fields if this policy only writes relationship properties.
A complete write-only policy for the running example: see assets/policy-annotate-played-at.json.
Create it through the Config API:
# set the current project_id, and stringify only the `policy` field, before POSTing
jq --arg pid "$PROJECT_GID" '.project_id = $pid | .policy |= tojson' indykite-ciq-add-relationship-property/assets/policy-annotate-played-at.json \
| curl -X POST "$API_URL/configs/v1/authorization-policies" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $SERVICE_ACCOUNT_TOKEN" \
-d @-
A 201 Created returns the policy's id (GID). Export it as POLICY_ID - the Knowledge Query create injects it into policy_id.
For the full schema (why we omit relationship_types, the Person variant, the protected property names) see references/policy-reference.md.
3. Create the Knowledge Query with upsert_relationships
The Knowledge Query references the policy. Each entry in upsert_relationships describes one relationship-update:
name- must match a relationship variable from the policy'scypher(e.g.r). This is what differs structurally from the create-relationship skill; using a fresh name here would imply create.source/target/type- omit when updating an existing relationship. The endpoints and label come from the matched edge; specifying them is unnecessary and can confuse the platform.properties- array of{type, value, metadata?}items. Same shape as for nodes.
Echo the result back in the response by listing properties to project in the top-level relationships and/or nodes arrays.
A complete Knowledge Query for the running example: see assets/knowledge-query-annotate-played-at.json.
Create it through the Config API:
# set the current project_id and policy_id, and stringify only the `query` field, before POSTing
jq --arg pid "$PROJECT_GID" --arg polid "$POLICY_ID" '.project_id = $pid | .policy_id = $polid | .query |= tojson' indykite-ciq-add-relationship-property/assets/knowledge-query-annotate-played-at.json \
| curl -X POST "$API_URL/configs/v1/knowledge-queries" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $SERVICE_ACCOUNT_TOKEN" \
-d @-
A 201 Created returns the Knowledge Query's id (GID).
Schema details - including the protected property names you cannot set (_service, create_time, id, type, update_time) and the metadata sub-array - live in references/knowledge-query-reference.md.
4. Authenticate and execute
The execute endpoint is the same as for reads, node-property-writes, and the create skills:
POST <API_URL>/contx-iq/v1/execute
Authentication for the running _Application-subject example:
X-IK-ClientKey: <AppAgent-credentials-token>- required.Authorization: Bearer …- omit for_Application.
For Person-subject relationship-property writes, add Authorization: Bearer <user-access-token>.
Request:
{
"id": "<knowledge_query_gid_or_name>",
"input_params": {
"track_external_id": "track-99",
"venue_external_id": "venue-1",
"first_played_at": "2026-04-22T19:00:00Z"
}
}
A runnable shell helper: scripts/execute.sh.
Full execute reference: references/execution-reference.md.
5. Verify the response and confirm the property write
A successful relationship-property write returns the projection you requested:
{
"data": [
{
"relationships": {
"r": {
"Id": 1152932499723124700,
"ElementId": "5:3a2b09d5-…:1152932499723124736",
"Props": {
"verified": true,
"first_played_at": "2026-04-22T19:00:00Z"
}
}
}
}
]
}
If the response is not what you expected, walk this list:
- Variable in
existing_relationships. The KQ'supsert_relationships[].namemust be in the policy'sexisting_relationshipslist. Mismatch →403. - Cypher matched a relationship. If the cypher returns no rows (e.g. the source or target
external_idisn't seeded, or the:PLAYED_ATedge doesn't exist), the upsert has nothing to attach to -200with emptydata. namematches a cypher variable. Using a fresh name implies create; rejected unlessrelationship_typesis also declared.- No
source/target/typein theupsert_relationshipsentry. Including any of these flips the operation to "create" semantics. - Property names not protected.
_service,create_time,id,type,update_timecannot be set as relationship properties.
For other failure modes see references/troubleshooting.md.
Outcome
When this skill has been applied successfully:
- A relationship-property-write CIQ policy exists; it has a single
subject.type, a Cypher pattern that matches the relationship to update, partial filters pinning the endpoints byexternal_id, and anallowed_upserts.relationships.existing_relationshipswhitelist. - A Knowledge Query references that policy and lists
upsert_relationshipsentries that reuse cypher variable names, omitsource/target/type, and declare the properties to set. POST /contx-iq/v1/executereturns the projected property values, confirming the write.- A follow-up read (e.g. via
indykite-ciq-read) finds the new property values on the relationship.
Files in this skill
references/policy-reference.md- policy schema,existing_relationshipsdeep-dive, why other blocks are omitted.references/knowledge-query-reference.md-upsert_relationshipsfor updates (variable from cypher, nosource/target/type), properties + metadata, protected names.references/execution-reference.md-POST /contx-iq/v1/executefor relationship-property writes, response shape with the relationship'sPropsblock.references/troubleshooting.md- symptom → fix tables.assets/policy-annotate-played-at.json- runnable_Applicationannotates:PLAYED_ATpolicy.assets/knowledge-query-annotate-played-at.json- matching Knowledge Query.scripts/execute.sh- Bash helper.
Agent-specific notes
This skill uses generic markdown instructions and works across all agents listed in the README. The agent needs to be able to issue HTTP requests (curl, an HTTP client, or the IndyKite Terraform provider). No Claude Code hooks, Cursor @-mentions, or Copilot workspace context are required.
References
- ContX IQ guide (developer hub) - full schema, including
allowed_upserts.relationships.existing_relationships. - Music dataset tutorial - Chapter 8 "ContX IQ policies" and Chapter 9 "Knowledge Queries" - read/write/delete variant naming convention.
- Developer-hub resources - CIQ examples - the
policyMetaData/knowledgeQueryMetaDatapair shows the analogous node-property write; this skill applies the same pattern to relationship variables. - Config API documentation
- Cypher query language manual (Neo4j; openCypher) - the graph query language used in CIQ policy and Knowledge Query conditions over the IndyKite Knowledge Graph.
- IndyKite Terraform provider