---
name: indykite-ciq-add-relationship-property
description: Author an IndyKite ContX IQ (CIQ) policy plus its Knowledge Query that sets one or more properties on an existing relationship in the IndyKite Graph (IKG), then run it via `POST /contx-iq/v1/execute`. Use when adding a brand-new property, overwriting an existing one, or attaching property metadata on a relationship that's already in the IKG - no relationship creation, no node writes, no deletes.
license: Apache-2.0
compatibility: Requires curl, bash 4+, and jq. Network access to the regional IndyKite REST API (eu.api.indykite.com or us.api.indykite.com) is required at runtime.
---

# 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`](../indykite-ciq-add-property/SKILL.md), 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_types` and a Knowledge Query `upsert_relationships` entry with a fresh `name` + `source`/`target`/`type` - see [`indykite-ciq-create-relationship`](../indykite-ciq-create-relationship/SKILL.md).
- **Setting properties on a node** uses `allowed_upserts.nodes.existing_nodes` - see [`indykite-ciq-add-property`](../indykite-ciq-add-property/SKILL.md).
- **Deleting a property on a relationship** uses `allowed_deletes.relationships` with a `<var>.<property>` path - see [`indykite-ciq-delete`](../indykite-ciq-delete/SKILL.md).

For reads, see [`indykite-ciq-read`](../indykite-ciq-read/SKILL.md).

## 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: true` to an existing `:PLAYED_AT`, set `weight` on an existing `:LIKES`, attach `confidence` metadata to an existing `:OWNS` edge);
- is annotating an existing relationship with provenance, trust score, or audit fields after the fact;
- is debugging a `403` / `422` from 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`](../indykite-ciq-create-relationship/SKILL.md);
- wants to **set properties on a node** - use [`indykite-ciq-add-property`](../indykite-ciq-add-property/SKILL.md);
- wants to **delete** a property - use [`indykite-ciq-delete`](../indykite-ciq-delete/SKILL.md);
- wants to **read** data - use [`indykite-ciq-read`](../indykite-ciq-read/SKILL.md);
- 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-ClientKey` at 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-`_Application` subjects, 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`](../indykite-data-schema/SKILL.md)) - 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 `_Application` annotates an existing `(:Track)-[:PLAYED_AT]->(:Venue)` relationship by setting a `verified` flag and a `first_played_at` timestamp.

```cypher
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` - currently `1.0-ciq`.
- `subject.type` - `_Application` for the running example.
- `condition.cypher` and `condition.filter` - the cypher matches the existing relationship; the filter pins `subject.external_id = $_appId` (reserved) plus the source and target endpoints by `external_id`.
- `allowed_upserts.relationships.existing_relationships` - array of relationship variables from `cypher` whose properties the Knowledge Query may write. The Knowledge Query's `upsert_relationships[].name` must 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`](assets/policy-annotate-played-at.json).

Create it through the Config API:

```bash
# 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`](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's `cypher` (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`](assets/knowledge-query-annotate-played-at.json).

Create it through the Config API:

```bash
# 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`](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:

```text
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:

```json
{
  "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`](scripts/execute.sh).

Full execute reference: [`references/execution-reference.md`](references/execution-reference.md).

### 5. Verify the response and confirm the property write

A successful relationship-property write returns the projection you requested:

```json
{
  "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:

1. **Variable in `existing_relationships`.** The KQ's `upsert_relationships[].name` must be in the policy's `existing_relationships` list. Mismatch → `403`.
2. **Cypher matched a relationship.** If the cypher returns no rows (e.g. the source or target `external_id` isn't seeded, or the `:PLAYED_AT` edge doesn't exist), the upsert has nothing to attach to - `200` with empty `data`.
3. **`name` matches a cypher variable.** Using a fresh name implies create; rejected unless `relationship_types` is also declared.
4. **No `source` / `target` / `type` in the `upsert_relationships` entry.** Including any of these flips the operation to "create" semantics.
5. **Property names not protected.** `_service`, `create_time`, `id`, `type`, `update_time` cannot be set as relationship properties.

For other failure modes see [`references/troubleshooting.md`](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 by `external_id`, and an `allowed_upserts.relationships.existing_relationships` whitelist.
- A Knowledge Query references that policy and lists `upsert_relationships` entries that reuse cypher variable names, omit `source`/`target`/`type`, and declare the properties to set.
- `POST /contx-iq/v1/execute` returns the projected property values, confirming the write.
- A follow-up read (e.g. via [`indykite-ciq-read`](../indykite-ciq-read/SKILL.md)) finds the new property values on the relationship.

## Files in this skill

- [`references/policy-reference.md`](references/policy-reference.md) - policy schema, `existing_relationships` deep-dive, why other blocks are omitted.
- [`references/knowledge-query-reference.md`](references/knowledge-query-reference.md) - `upsert_relationships` for updates (variable from cypher, no `source`/`target`/`type`), properties + metadata, protected names.
- [`references/execution-reference.md`](references/execution-reference.md) - `POST /contx-iq/v1/execute` for relationship-property writes, response shape with the relationship's `Props` block.
- [`references/troubleshooting.md`](references/troubleshooting.md) - symptom → fix tables.
- [`assets/policy-annotate-played-at.json`](assets/policy-annotate-played-at.json) - runnable `_Application` annotates `:PLAYED_AT` policy.
- [`assets/knowledge-query-annotate-played-at.json`](assets/knowledge-query-annotate-played-at.json) - matching Knowledge Query.
- [`scripts/execute.sh`](scripts/execute.sh) - Bash helper.

## Agent-specific notes

This skill uses generic markdown instructions and works across all agents listed in the [README](../README.md). 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)](https://developer.indykite.com/guides/guide-contx-iq) - full schema, including `allowed_upserts.relationships.existing_relationships`.
- [Music dataset tutorial - Chapter 8 "ContX IQ policies" and Chapter 9 "Knowledge Queries"](https://developer.indykite.com/tutorials/tutorial-music-dataset) - read/write/delete variant naming convention.
- [Developer-hub resources - CIQ examples](https://developer.indykite.com/resources) - the `policyMetaData` / `knowledgeQueryMetaData` pair shows the analogous node-property write; this skill applies the same pattern to relationship variables.
- [Config API documentation](https://openapi.indykite.com/api-documentation-config)
- [Cypher query language manual (Neo4j; openCypher)](https://neo4j.com/docs/cypher-manual/current/) - the graph query language used in CIQ policy and Knowledge Query conditions over the IndyKite Knowledge Graph.
- [IndyKite Terraform provider](https://registry.terraform.io/providers/indykite/indykite/latest/docs)
