# IndyKite Developer Hub - full content > Single-file concatenation of every guide, code example, Terraform example, tutorial, and agent skill on https://developer.indykite.com. For the structured index (per-document URLs, JSON APIs), fetch /llms.txt instead. --- # Audit Signing: Control the Key That Signs Your Audit Records > Configure who manages the signing key for your project's audit records - the platform, or your own key in GCP KMS, AWS KMS, or Azure Key Vault - via the Config API. **Category:** Audit Signing ## Summary What an Audit Signing configuration is, the supported key providers, every field and its validation rules, and the full REST lifecycle: create, read, list, update with ETag, and delete. ## Content # What is Audit Signing? An **Audit Signing configuration** declares who manages the cryptographic key used to sign your project's audit records, making them tamper-evident. You can choose to have the platform manage the signing key for you; with a customer-managed provider, signing uses a key that lives in **your own** cloud key store - Google Cloud KMS, AWS KMS, or Azure Key Vault - so key custody, rotation, and revocation stay under your control. The configuration is a project-scoped Config API object, managed with Service Account credentials like other configurations (Token Introspect, MCP Server, Outbound Events). # Which key providers are supported? Provider Who holds the key Requires `PLATFORM_MANAGED` IndyKite manages the signing key. Only choose this `provider`. Nothing beyond `name` and `project_id`. `CUSTOMER_GCP_KMS` Your key in Google Cloud KMS. `key_resource` + `kid`, plus provider access material in `auth_params`. `CUSTOMER_AWS_KMS` Your key in AWS KMS. `key_resource` + `kid`, plus provider access material in `auth_params` (e.g. `role_arn`). `CUSTOMER_AZURE_KEY_VAULT` Your key in Azure Key Vault. `key_resource` + `kid`, plus provider access material in `auth_params`. # What fields does the configuration have? Field Type Description `name`string, requiredURL-friendly identifier, unique within the project. Immutable - it cannot be changed later. `project_id`string (GID), requiredProject that owns the configuration. `display_name`string, optional (2-254 chars)Human-readable name; equals `name` when not set. `description`string, optional (2-65000 chars)Free-text description. `provider`enum`PLATFORM_MANAGED` (if you want an IndyKite managed provider), `CUSTOMER_GCP_KMS`, `CUSTOMER_AWS_KMS`, or `CUSTOMER_AZURE_KEY_VAULT`. Required on update. `key_resource`string (max 256)Identifies the key in the provider's namespace (for AWS KMS, the key ARN). Required for every customer-managed provider. `kid`string (max 256)The key ID stamped on signatures so verifiers can select the right key. Required for every customer-managed provider. `auth_params`map of string to string (max 32 pairs)Provider access material (for AWS KMS, e.g. `role_arn`). **Write-only**: values are accepted on create/update but come back masked as empty strings on every read. # How do I create an Audit Signing configuration? All endpoints authenticate with a Service Account Bearer token, like the rest of the Config API. ## Platform-managed (minimal) curl -X POST /configs/v1/audit-signings -H "Content-Type: application/json" -H "Authorization: Bearer $SERVICE_ACCOUNT_TOKEN" -d '{ "name": "audit-signing-default", "project_id": "gid-of-project", "provider": "PLATFORM_MANAGED" }' ## Customer-managed key (AWS KMS example) curl -X POST /configs/v1/audit-signings -H "Content-Type: application/json" -H "Authorization: Bearer $SERVICE_ACCOUNT_TOKEN" -d '{ "name": "audit-signing-byok", "display_name": "Audit signing with our AWS KMS key", "description": "Signs audit records with the compliance team key", "project_id": "gid-of-project", "provider": "CUSTOMER_AWS_KMS", "key_resource": "arn:aws:kms:us-east-1:123456789012:key/example-key-id", "kid": "audit-2026-q3", "auth_params": { "role_arn": "arn:aws:iam::123456789012:role/indykite-audit-signer" } }' **Response (201 Created):** the new configuration's `id` (GID), `create_time`, `created_by`, `update_time`, and `updated_by`, plus an `ETag` response header for optimistic concurrency on later updates and deletes. Reference: POST /audit-signings # How do I read and list configurations? Read by GID, or by `name` with the `location` query parameter; an optional `version` query parameter reads a specific version: curl /configs/v1/audit-signings/?location= -H "Authorization: Bearer $SERVICE_ACCOUNT_TOKEN" The response includes the configuration fields plus `organization_id`, timestamps, and authorship - and **`auth_params` values come back masked as empty strings**; secrets never round-trip. List every configuration in a project with: curl /configs/v1/audit-signings?project_id= -H "Authorization: Bearer $SERVICE_ACCOUNT_TOKEN" The list response wraps the same objects in a `data` array. # How do I update a configuration? Send a `PUT` with an `If-Match` header carrying the ETag from the last read or write - a stale ETag returns `412 Precondition Failed`. `provider` is required on update; for `display_name` and `description`, `null` keeps the current value while an empty string removes it: curl -X PUT /configs/v1/audit-signings/ -H "Content-Type: application/json" -H "Authorization: Bearer $SERVICE_ACCOUNT_TOKEN" -H "If-Match: $ETAG" -d '{ "provider": "CUSTOMER_AWS_KMS", "key_resource": "arn:aws:kms:us-east-1:123456789012:key/rotated-key-id", "kid": "audit-2026-q4", "auth_params": { "role_arn": "arn:aws:iam::123456789012:role/indykite-audit-signer" } }' Rotating to a new key is exactly this: point `key_resource` at the new key and change the `kid` so verifiers can tell old signatures from new ones. # How do I delete a configuration? curl -X DELETE /configs/v1/audit-signings/?etag=$ETAG -H "Authorization: Bearer $SERVICE_ACCOUNT_TOKEN" **Response:** `204 No Content`. The `etag` query parameter is optional but recommended - like `If-Match` on update, it prevents deleting a configuration someone else just changed. # What errors should I expect? Status Meaning `422 Unprocessable Entity`Validation failed; the body carries a `message` and an `errors` array with the exact reasons: `provider is required`, `key_resource is required for customer-managed providers`, `kid is required for customer-managed providers`. `412 Precondition Failed`The `If-Match` ETag (or `etag` query parameter on delete) no longer matches - re-read the configuration and retry. `409 Conflict`A configuration with the same `name` already exists in the project. `404 Not Found`No configuration with that ID or name (including one already deleted). `401` / `403`Missing or insufficient Service Account credentials. # Next Steps - Worked example: Audit Signing Resource Example - Environment setup: Environment Guide - Credentials: Credentials Guide - API reference: Audit Signing endpoints --- Source: https://developer.indykite.com/guides/guide-audit-signing --- # What is AuthZEN? > Which problems AuthZEN requests solve and how to use them. **Category:** KBAC ## Summary Description of AuthZEN requests and what they solve. ## Content # What is AuthZEN? **AuthZEN** (AuthZ ENhancement) is an OpenID Foundation initiative that standardizes fine-grained authorization, similar to how OpenID Connect standardized authentication. AuthZEN creates interoperability between: - **Policy Enforcement Points (PEPs)**: Applications that need authorization decisions. - **Policy Decision Points (PDPs)**: Services that evaluate policies and return decisions. IndyKite is an AuthZEN-compliant PDP that uses its Identity Knowledge Graph (IKG) to make intelligent, context-aware authorization decisions. # Why use AuthZEN? ## What problem does AuthZEN solve for interoperability? **Problem:** The authorization landscape is fragmented. Different vendors use proprietary APIs, protocols, and policy languages, creating vendor lock-in and integration challenges. **AuthZEN's Solution:** A standardized communication protocol between PEPs and PDPs. Any application can query any AuthZEN-compliant PDP, regardless of the underlying policy engine. **IndyKite Benefit:** Organizations can use IndyKite's advanced KBAC capabilities alongside other AuthZEN-compliant components without vendor lock-in. ## How does AuthZEN simplify development? **Problem:** Developers often "reinvent the authorization wheel" for each application, leading to inconsistent implementations and security vulnerabilities. **AuthZEN's Solution:** Standardized JSON-based request/response formats with core entities: - `subject`: Who is requesting access - `action`: What operation they want to perform - `resource`: What they want to access - `context`: Additional environmental data **IndyKite Benefit:** Developers use the standard AuthZEN interface instead of learning a proprietary API, reducing integration time. ## Why externalize authorization? **Problem:** Embedding authorization logic in applications makes it difficult to manage, update, and audit. Fine-grained access control (e.g., "Can Alice view this specific document at this time?") becomes unscalable. **AuthZEN's Solution:** Externalize authorization to dedicated PDPs that make dynamic, real-time decisions based on subject, resource, action, and context. **IndyKite Benefit:** IndyKite's Identity Knowledge Graph enables complex, relationship-based authorization. Access decisions are based on "who you are, what you're trying to do, with what, from where, and why" — not just static roles. ## How does AuthZEN improve security and compliance? **Problem:** Inconsistent authorization creates security gaps and makes compliance difficult to demonstrate. **AuthZEN's Solution:** Continuous authorization enforcement, dynamic separation of duties, and clear audit trails. **IndyKite Benefit:** The IKG continuously updates with real-time data, enabling dynamic and adaptive access decisions that support "least privilege" and "zero trust" policies. # How do I make an AuthZEN request? ## Single Evaluation Request **Endpoint:** - EU: `POST https://eu.api.indykite.com/access/v1/evaluation` - US: `POST https://us.api.indykite.com/access/v1/evaluation` ### Request Syntax { "subject": { "type": "", "id": "" }, "resource": { "type": "", "id": "" }, "action": { "name": "" }, "context": { "input_params": { "": "" } } } ### What does each field mean? **Field** **Description** **Example** `subject.type` The type of entity requesting access (maps to node label in IKG) `"User"`, `"Service"` `subject.id` Unique identifier for the subject (maps to external_id in IKG) `"alice@example.com"` `resource.type` The type of resource being accessed (maps to node label in IKG) `"Document"`, `"API"` `resource.id` Unique identifier for the resource (maps to external_id in IKG) `"project_alpha_specs.pdf"` `action.name` The operation being requested `"CAN_READ"`, `"CAN_EDIT"` `context.input_params` Additional contextual data for policy evaluation. Also carries **location parameters** for `3.0-kbac` policies on a composite IKG (see below) `{"ip_address": "192.168.1.100"}`, `{"region": "east"}` ### Request Example { "subject": { "type": "User", "id": "alice@example.com" }, "resource": { "type": "Document", "id": "project_alpha_specs.pdf" }, "action": { "name": "CAN_READ" }, "context": { "input_params": { "ip_address": "192.168.1.100", "time_of_day": "14:30:00Z", "device_type": "laptop" } } } ### Response Syntax { "decision": , "context": { "advice": [{ "error": "", "error_description": "" }] } } ### What does the response contain? **Field** **Description** `decision` `true` = access allowed, `false` = access denied `context.advice` Optional array of advice objects explaining why access was denied `advice.error` Error code (e.g., `"insufficient_user_authentication"`) `advice.error_description` Human-readable explanation of the denial ### Response Example (Denied) { "decision": false, "context": { "advice": [{ "error": "insufficient_user_authentication", "error_description": "Authentication is expired" }] } } # How does IndyKite process an AuthZEN request? When IndyKite receives an AuthZEN request, it: - **Maps request to graph entities**: The `subject`, `resource`, `action`, and `context` are mapped to nodes and relationships in the Identity Knowledge Graph. **Evaluates KBAC policies**: Policies defined as graph traversals are evaluated against real-time data. Example policy logic: - "Allow `User` to `CAN_READ` `Document` IF `User` `IS_MEMBER_OF` `Team` AND `Document` `IS_ASSIGNED_TO` `Project` AND `Team` `IS_ASSIGNED_TO` `Project`" - "Deny access IF `context.ip_address` is NOT in `trusted_network`" - **Returns decision**: An AuthZEN-compliant response with `decision` (true/false) and optional `advice` explaining the decision. # How do I batch multiple evaluations? Use the batch endpoint to evaluate multiple authorization requests in a single call. **Endpoint:** - EU: `POST https://eu.api.indykite.com/access/v1/evaluations` - US: `POST https://us.api.indykite.com/access/v1/evaluations` ### Batch Request Syntax { "subject": { "type": "", "id": "" }, "resource": { "type": "", "id": "" }, "action": { "name": "" }, "context": { "input_params": {}, "policy_tags": [""] }, "evaluations": [{ "subject": { "type": "", "id": "" }, "resource": { "type": "", "id": "" }, "action": { "name": "" }, "context": { "input_params": {}, "policy_tags": [""] } }] } ### How do default values work in batch requests? Fields defined at the top level serve as defaults for all evaluations. Each evaluation can override these defaults. **Field** **Required** **Description** `subject` Optional Default subject for all evaluations. Useful when the same user accesses multiple resources. `resource` Optional Default resource for all evaluations. Overridden by evaluation-specific resource. `action` Optional Default action for all evaluations. Overridden by evaluation-specific action. `context` Optional Default context for all evaluations. Overridden by evaluation-specific context. `evaluations` Required Array of individual evaluation objects to process. ### What are policy_tags? The `policy_tags` field allows you to filter which policies are evaluated for a request. Only policies with matching tags will be considered. ### Batch Request Example { "subject": { "type": "User", "id": "alice@example.com" }, "action": { "name": "CAN_READ" }, "evaluations": [ { "resource": { "type": "Document", "id": "doc_001" } }, { "resource": { "type": "Document", "id": "doc_002" } }, { "resource": { "type": "Folder", "id": "folder_001" }, "action": { "name": "CAN_LIST" } } ] } This example evaluates: - Can Alice READ doc_001? - Can Alice READ doc_002? - Can Alice LIST folder_001? (overrides default action) # Which KBAC policy versions answer AuthZEN requests? The decisions behind every AuthZEN endpoint come from KBAC policies. Two policy versions exist, selected by `meta.policy_version` in the policy JSON: **Aspect** `2.0-kbac` `3.0-kbac` Condition Cypher handling Rewritten by the platform into evaluation and search variants **Raw**: runs as authored; the platform only pins subject/resource and appends the projection Composite-database routing (data residency) None: always the default database `USE graph.byName(...)` clauses, static or via **location parameters** supplied in `context.input_params` Allowed Cypher clauses No `CALL`, no `RETURN` `CALL { }` subqueries and inner `RETURN`s allowed; mutating clauses still blocked Subject node requirement Must be an **identity node** (`is_identity: true` at ingest) Any node: matched by type and external ID User (bearer) token at decision time Optional; binds the subject to the token's identity (internal-node-ID pin, exposed as `$subject_id`) Optional; the token's subject must equal the requested subject or the call is denied with `403 Forbidden` External (resolver-backed) properties in the condition Allowed Rejected at creation Policy JSON schema Identical: only `meta.policy_version` differs **2.0-kbac subjects must be identity nodes**: the subject node must have been ingested with `is_identity: true` (Capture API). A subject ingested as a plain entity never matches a `2.0-kbac` condition - the decision is `false` with no error, which makes this the first thing to check when a correct-looking policy always denies. `3.0-kbac` matches the subject by type and external ID only and does not require `is_identity`. **3.0-kbac does not require a composite database**: only `USE` routing (static or dynamic) does. A `3.0-kbac` policy without a `USE` clause evaluates against the default database as plain raw Cypher. **A valid 2.0-kbac condition is also a valid 3.0-kbac condition**: you can carry a policy over just by changing `meta.policy_version`, with three caveats: `$subject_id` must not be referenced (see below), external (resolver-backed) properties are rejected at creation, and the platform-bound parameters (`$subject_external_id`, `$subject_type`, `$resource_external_id`, `$resource_type`) are bound automatically and never supplied via `input_params`. ### $subject_id is not available in 3.0-kbac A `3.0-kbac` condition must not reference `$subject_id`: on a composite IKG the subject's internal node ID is not stable across locations, so `3.0-kbac` identifies subjects by **type and external ID** only. Creating a policy that references it fails with `422 Unprocessable Entity`: { "message": "Unprocessable Entity", "errors": [ "invalid policy config: parameter \"$subject_id\" is reserved and cannot be referenced" ] } No replacement is needed: the platform already pins the subject by type and external ID in every `3.0-kbac` query. On `2.0-kbac`, `$subject_id` names the internal-node-ID pin applied when the request carries a user token: the platform binds the subject to the token's identity automatically. A `2.0-kbac` condition can reference it, but it then counts as a regular input parameter - the request must carry `subject_id` under `context.input_params`, and a user token on the request overrides the supplied value with the token identity's internal node ID. Since the pin already happens automatically, conditions rarely need to reference it. ### Optional condition.filter: graph-free pre-checks Next to `cypher`, a policy's `condition` accepts an optional `filter`: a boolean expression tree evaluated without touching the graph, against the request's `context.input_params` and the user token's claims. The decision is `true` only when **both** the Cypher condition and the filter hold. It works identically on `2.0-kbac` and `3.0-kbac`. - Branch nodes combine `operands` with `AND` / `OR` (two or more operands) or `NOT` (exactly one). - Leaf nodes compare an `attribute` against a `value` with `=`, `<>`, `<`, `<=`, `>`, `>=`, `IN` (array value), `=~`, `STARTS WITH`, `ENDS WITH`, `CONTAINS`, `IS NULL`, `IS NOT NULL`. - `"$token."` resolves to a user-token claim and `"$name"` to an input param (dot paths reach into object params); any other JSON value is a literal. Wrap datetime comparisons as `{"type": "datetime", "value": ""}`. - A leaf may carry an `advice` map of string key/values, returned under `context.advice` when the filter denies (see resource `authz-3`). "condition": { "cypher": "MATCH (subject:Person)-[:CAN_AFFORD]->(resource:Server)", "filter": { "operator": "AND", "operands": [ { "operator": "=", "attribute": "$token.plan", "value": "premium" }, { "operator": "IN", "attribute": "$channel", "value": ["web", "mobile"], "advice": { "error": "unsupported_channel" } } ] } } ### How do I request a decision in a specific location? On a composite IKG (see the data residency guide), a `3.0-kbac` policy can route with a dynamic parameter, for example `USE graph.byName($region)`. The AuthZEN request then supplies the **logical location** (a key of the project's `alias_mapping`) through `context.input_params`: no new endpoint or field is involved: { "subject": { "type": "Person", "id": "person-alice" }, "resource": { "type": "Car", "id": "car-kitt" }, "action": { "name": "CAN_DRIVE" }, "context": { "input_params": { "region": "east" } } } This works identically on `/access/v1/evaluation`, `/access/v1/evaluations` (put the location in the default or per-evaluation context), and the three search endpoints. Failure modes: - Omitting a required location parameter, passing a non-string, or naming a location that is not in `alias_mapping` fails the call with `422 Unprocessable Entity`. - Routing with `USE` (static or via a location parameter) against a project without a composite database fails with `422 Unprocessable Entity` (policy requires a composite database). - Bearer-token calls where the token's subject differs from the requested `subject` are denied with `403 Forbidden`. # What credentials do I need? - **AppAgent credentials**: Required for all AuthZEN requests. - **User access token**: Required if subject is a user (not _Application). Authentication header: `X-IK-ClientKey: ` # Next Steps - KBAC concepts: Dynamic Authorization Guide - Data residency and 3.0-kbac routing: Data Residency Guide - Full examples: Developer Hub Resources - Credentials guide: Credentials Guide - OpenID AuthZEN specification: https://openid.net/wg/authzen/ --- Source: https://developer.indykite.com/guides/guide-authzen --- # Cypher for ContX IQ: Policy and Knowledge Query Patterns > How to write the Cypher used in CIQ policies and Knowledge Queries - the Neo4j-adapted dialect, its rules, and worked examples. **Category:** ContX IQ ## Summary A practical, agent-friendly reference for the Cypher dialect inside CIQ policy condition.cypher: what is supported, how it differs from standard Neo4j Cypher, how Cypher variables bind to a Knowledge Query, and copy-paste examples from simple reads to aggregation. ## Content # What is Cypher in ContX IQ? **Cypher** is the graph pattern-matching language created for Neo4j. ContX IQ (CIQ) embeds a **Neo4j-adapted subset** of Cypher inside a policy's `condition.cypher` field. You use it to describe **the contextual subgraph** a request is allowed to touch: which nodes, which relationships, and how they connect. The most important idea to internalize: in CIQ, Cypher only **matches** context. It does **not** return, create, or delete data. Reads, writes, and deletes are declared separately in the **Knowledge Query** and gated by the policy's `allowed_reads`, `allowed_upserts`, and `allowed_deletes`. Cypher names the pieces; the Knowledge Query decides what to do with them. **Schema example** This guide assumes you already know the CIQ component model (Policy → Knowledge Query → Execution). If not, read ContX IQ: Context-Aware Data Queries and Policies first. For a gentle introduction to graph traversal and Cypher basics, see Why use a Graph Database? # Where do I write Cypher? Cypher lives in exactly one place: the `condition.cypher` string of a policy. { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "Person" }, "condition": { "cypher": "MATCH (subject:Person)-[r:LIKES]->(track:Track)", "filter": [ ... ] }, "allowed_reads": { "nodes": ["track.property.title"], "relationships": ["r"] } } The **Knowledge Query** then references the variables you named in the Cypher (`subject`, `track`, `r`) - it never re-declares the pattern. # Which policy version does this cover? Everything in this guide targets CIQ **v1** - `policy_version: "1.0-ciq"`, set in the policy's `meta`. This is the stable, default dialect and the one you should use. A **v2** dialect - **2.0 (Beta testing only)** - is also accepted by the runtime and available on request; it is the engine intended to become the next stable version. v2 is **not a new query language**. The external API and authentication, the full filter operator set and value types, every Cypher construct in this guide (`MATCH` / `OPTIONAL MATCH` / `WHERE` / `WITH`, aggregates, variable-length paths, inline `$param`s, `trust_score` and `_Application` handling), and the Knowledge Query and execution shapes are all **identical** to v1. **Every example in this guide is valid on both versions.** v2 changes little, in two areas. First, **stricter validation when a policy is created** - it rejects two patterns that v1 quietly accepted: - **No references to anonymous nodes.** Any node referenced from a `filter`, an `allowed_*` list, or the Knowledge Query must be named in the Cypher. Referencing an unnamed node - e.g. the `(:Track)` in `MATCH (subject:Person)-[:LIKES]->(:Track)` - is rejected; write `(track:Track)`. (A node always needs a label in both versions; v2 additionally forbids *referencing* an unnamed one.) - **No untyped relationships in writes or deletes.** A relationship feeding `allowed_upserts` or `allowed_deletes` must declare its type in the Cypher (`[r:PLAYED_AT]`, not bare `[r]`). An untyped relationship is still fine when used only in reads. Both rules are good practice on v1 too, so the patterns in this guide already satisfy them - a v1 policy written this way creates unchanged under v2. Second, one **filter extension**: v2 lets the filter `attribute` side also be an `@` reference (v1 allows that only on the `value` side). The two dialects can coexist in one project once v2 is enabled. # How is CIQ Cypher different from standard Neo4j Cypher? CIQ Cypher is the **read/traverse core** of Cypher, with the result and mutation clauses removed and replaced by declarative JSON. Keep these adaptations in mind: **Standard Cypher** **In CIQ** `RETURN` chooses output No `RETURN`. Output is chosen by the Knowledge Query's `nodes`, `relationships`, and `aggregate_values` - and must be allow-listed in policy `allowed_reads`. `CREATE` / `MERGE` / `SET` Not written in Cypher. Declared in the Knowledge Query's `upsert_nodes` / `upsert_relationships`, gated by policy `allowed_upserts`. `DELETE` / `REMOVE` Not written in Cypher. Declared in the Knowledge Query's `delete_nodes` / `delete_relationships`, gated by policy `allowed_deletes`. Any node can be a starting point Exactly one node must be the **subject** - the variable named `subject`, with a label matching `subject.type`. A policy has a single subject type. Anonymous nodes are common Any node or relationship you reference later **must be named** (give it a variable). Anonymous parts can only be used as connective glue. Parameters are `$x` bound by the driver Parameters are `$x` placeholders resolved at execution from `input_params`. `$token.*` (token claims) and `$_appId` (application identity) are reserved. Filtering via `WHERE` Inline `WHERE` works, but execution-time parameters and token claims are best expressed in the structured `filter` array (see below). # What Cypher syntax is supported? CIQ supports the pattern-matching and projection core. Treat this as the practical surface; validate edge cases by creating the policy through the Config API (it rejects unsupported or invalid constructs). **Feature** **Example** `MATCH` `MATCH (subject:Person)-[r:LIKES]->(track:Track)` `OPTIONAL MATCH` `OPTIONAL MATCH (subject)-[:CREATED]->(p:Playlist)` Multiple comma / multi-clause patterns (cartesian + join) `MATCH (subject:Person), (venue:Venue)` `WHERE` with comparisons and boolean logic `WHERE subject.property.karaoke_confidence >= venue.property.min_confidence` `WITH` (projection / pipelining; exposes aggregates) `WITH subject, COLLECT(track.external_id) AS liked` Relationship direction `-[r]->` outgoing, `<-[r]-` incoming, `-[r]-` either Relationship type and alternation `[:SUBSCRIBED_TO|CREATED]` Variable-length paths (bound the depth - see the timeout note) `[rels:INVOKES*1..5]`, `[:MEMBER_OF*1..3]` Inline property maps `(venue:Venue {name: $venue_name})`, `(cp:Property {type:'role'})` Multiple labels and re-using a bound variable `(n:Label1:Label2)`; close a loop with an already-bound name, e.g. `...<-[:CREATED]-(subject)` Aggregate functions `COUNT(...)`, `COUNT(DISTINCT ...)`, `COLLECT(...)`, `SUM` / `AVG` / `MIN` / `MAX` Predicate & list functions `ALL` / `ANY` / `NONE`, list comprehension `[r IN rels | endNode(r).external_id]`, `endNode()` / `startNode()` # How do Cypher variables flow into a Knowledge Query? The variable names in `condition.cypher` are the contract between the policy and everything downstream. A name you bind in Cypher becomes the handle used by: - **Policy `allowed_reads` / `allowed_upserts` / `allowed_deletes`** - the allow-list of what may be read, written, or removed. - **Knowledge Query `nodes` / `relationships` / `aggregate_values`** - what this query actually returns. - **Knowledge Query `upsert_*` / `delete_*`** - the variables to mutate. A read variable must be allow-listed in the policy **and** requested in the query. If the Cypher binds `track` but `allowed_reads.nodes` omits it, the query cannot return it. ### What attribute naming conventions are used? **Pattern** **Description** **Example** `.property.` A property value on a node `track.property.title` `.external_id` The external id of a node or relationship `subject.external_id` `.property..metadata.` Metadata attached to a property `ln.property.value.metadata.source` `.trust_score.` A Trust Score signal on a node (see the Trust Score guide) `subject.trust_score._final_score` `.*` / `.property.*` Wildcard - all of a node's properties `subject.*` `$token.` A claim on the requestor's token `$token.sub`, `$token.acr`, `$token.iat`, `$token.scope` `$` An execution-time parameter from `input_params` `$venue_name` `$_appId` Reserved - auto-filled with the calling Application's `external_id` `subject.external_id = $_appId` # Worked examples (music dataset) Each example shows the policy `condition` (Cypher + filter), the matching Knowledge Query, and the execution `input_params`. They use the IndyKite music sandbox dataset (Artists, Tracks, Albums, Playlists, Venues, People). ## 1. Subject only - read your own profile The simplest pattern: match just the subject and pin it to the caller's token via a `filter`. No relationships needed. `MATCH (subject:Person)` // policy condition "condition": { "cypher": "MATCH (subject:Person)", "filter": [ { "operator": "=", "attribute": "subject.external_id", "value": "$token.sub" } ] } // knowledge query - read selected profile properties { "nodes": ["subject.property.firstname", "subject.property.email", "subject.property.city"], "relationships": [] } // execution - no params; identity comes from the token { "id": "kq-person-own-profile-read", "input_params": {} } The `$token.sub` filter guarantees a Person can only ever resolve *their own* node, even though the Cypher matches the label generically. ## 2. One hop - a Person's liked tracks Name the relationship (`r`) so the query can both filter on the connected node and return the edge. `MATCH (subject:Person)-[r:LIKES]->(track:Track)` "condition": { "cypher": "MATCH (subject:Person)-[r:LIKES]->(track:Track)", "filter": [ { "operator": "AND", "operands": [ { "operator": "=", "attribute": "subject.external_id", "value": "$token.sub" }, { "operator": "=", "attribute": "track.external_id", "value": "$track_external_id" } ]} ] } // knowledge query { "nodes": ["track.property.title", "track.property.popularity"], "relationships": ["r"] } // execution { "id": "kq-person-liked-tracks-read", "input_params": { "track_external_id": "track-1" } } ## 3. _Application subject with parameters When the subject is `_Application`, bind it and constrain it with `$_appId` (auto-filled). A second `MATCH` brings in the data graph; a named edge (`r`) lets you read and later mutate it. `MATCH (subject:_Application) MATCH (track:Track)-[r:PLAYED_AT]->(venue:Venue)` "condition": { "cypher": "MATCH (subject:_Application) MATCH (track:Track)-[r:PLAYED_AT]->(venue:Venue)", "filter": [ { "operator": "AND", "operands": [ { "attribute": "venue.property.name", "operator": "=", "value": "$venue_name" }, { "attribute": "subject.external_id", "operator": "=", "value": "$_appId" } ]} ] } // read query { "nodes": ["track.property.title", "track.property.loudness", "track.property.energy"], "relationships": ["r"] } // execution - X-IK-ClientKey only; $_appId is implicit, never sent { "id": "kq-app-venue-playable-tracks-read", "input_params": { "venue_name": "Shower-Concert-Hall" } } ## 4. Writing and deleting reuse the same Cypher variables The same policy supports mutation Knowledge Queries - no new Cypher. To **create** a relationship, reference the node variables as `source`/`target`; to **delete**, reference the named edge. The policy must allow these via `allowed_upserts` / `allowed_deletes`. // upsert: link track -> venue with a new PLAYED_AT edge (gated by allowed_upserts) { "upsert_relationships": [ { "name": "newPlayedAt", "source": "track", "target": "venue", "type": "PLAYED_AT" } ], "nodes": ["track.external_id", "venue.external_id"], "relationships": ["newPlayedAt"] } // delete: remove the matched PLAYED_AT edge r (gated by allowed_deletes) { "delete_relationships": ["r"], "nodes": ["track.external_id"] } Updating a node works the same way - reference the Cypher variable by `name` in `upsert_nodes` (e.g. set new properties on `subject` or `playlist`). Protected properties cannot be deleted: `_service`, `create_time`, `external_id`, `id`, `type`, `update_time`. **Identity nodes.** An `upsert_nodes` entry accepts an optional `is_identity` boolean (default `false`). Set `"is_identity": true` to upsert the node as an identity node - the same result as `is_identity: true` in the Capture API. Adding `"labels": ["DigitalTwin"]` to the entry is equivalent (the flag is shorthand for adding this label; combining both is harmless), but prefer the boolean - unlike a label, it cannot be mistyped. Mark the node whenever it must act as a `2.0-kbac` authorization subject: a non-identity subject makes every `2.0-kbac` decision silently `false` (`3.0-kbac` matches by type and external ID and does not need it). The label goes in `labels` only - never in the policy's `allowed_upserts.nodes.node_types` whitelist, which checks `type`. Label values are not validated, so a typo such as `Digitaltwin` is accepted silently and only surfaces when `2.0-kbac` decisions come back `false` - after creating, verify with a `2.0-kbac` evaluation that uses the new node as subject. Note that CIQ cannot bootstrap its own subject: the execute call authorizes against an existing subject node, so the first identity in a flow - the one the request runs as - must be ingested through the Capture API (`is_identity: true`). **What the write/delete returns.** The response shape depends on what the query *reads*, not on whether it writes or deletes. The examples above list read-back fields (`nodes` / `relationships`), so they return that data. A write or delete that lists **nothing** to read (empty `nodes`, `relationships`, `aggregate_values`) instead returns a single **count** of affected rows under the reserved `result` key: `{ "data": [ { "aggregate_values": { "result": 3 } } ] }`. To get the changed entities back, list them to read. This is the same in both versions. ## 5. Mixed direction and multi-hop Patterns can change direction mid-chain. Here the caller's playlist is reached outgoing (`CREATED`), and its tracks point *into* the playlist (`<-[pf:PART_OF]-`). `MATCH (subject:Person)-[r:CREATED]->(playlist:Playlist)<-[pf:PART_OF]-(track:Track)` "filter": [ { "operator": "AND", "operands": [ { "operator": "=", "attribute": "subject.external_id", "value": "$token.sub" }, { "operator": "=", "attribute": "playlist.external_id", "value": "$playlist_external_id" } ]} ] // knowledge query - read the incoming edge pf and track titles { "nodes": ["playlist.property.name", "track.property.title", "track.external_id"], "relationships": ["pf"] } // execution { "id": "kq-person-playlist-tracks-read", "input_params": { "playlist_external_id": "playlist-100" } } ## 6. Relationship alternation and undirected hops A relationship segment can match **any of several types** (alternation with `|`) and can be **undirected** (`-[...]-`, matching either direction). This policy lets a Person read playlists created by a family member, where "family" is any of three relationship types in either direction. `MATCH (subject:Person)-[:MARRIED_TO|PARTNERS|PARENT_OF]-(family:Person)-[:CREATED]->(playlist:Playlist)` "condition": { "cypher": "MATCH (subject:Person)-[:MARRIED_TO|PARTNERS|PARENT_OF]-(family:Person)-[:CREATED]->(playlist:Playlist)", "filter": [ { "operator": "=", "attribute": "subject.external_id", "value": "$token.sub" } ] } // knowledge query { "nodes": ["family.property.firstname", "playlist.property.name", "playlist.property.mood"], "relationships": [] } // execution - identity comes from the token, no params { "id": "kq-person-family-playlists", "input_params": {} } An inline `WHERE` is also fine for static thresholds, e.g. `... WHERE subject.property.karaoke_confidence >= 0.8`. ## 7. Aggregation with WITH and aggregate_values Use `WITH` to project aggregates, then expose them via the Knowledge Query's `aggregate_values`. This _Application policy walks a variable-length `INVOKES` chain from a workflow to a target agent (anchored by the agent's indexed `external_id`) and collects the agent ids. The depth is **bounded** (`*1..5`) so the traversal stays fast - see Will my query time out? below. MATCH (subject:_Application) MATCH (wf:Workflow)-[rels:INVOKES*1..5]->(a:Agent {external_id: $agent_id}) WHERE ALL(r IN rels WHERE r.workflow_name = wf.external_id AND endNode(r):Agent) WITH subject, wf.external_id AS workflow, [r IN rels | endNode(r).external_id] AS agent_list // the aliases created by WITH become the readable aggregates "allowed_reads": { "nodes": [], "relationships": [], "aggregate_values": ["workflow", "agent_list"] } // knowledge query returns only the aggregates { "aggregate_values": ["workflow", "agent_list"] } // execution { "id": "kq-agent-invocation-chain", "input_params": { "agent_id": "agent-42" } } Whatever you alias in the final `WITH` (`... AS workflow`, `... AS agent_list`) is what `aggregate_values` can read - this is how aggregate results leave the query, since there is no `RETURN`. ## 8. OPTIONAL MATCH and counting `OPTIONAL MATCH` includes data that may or may not exist (it yields null rather than dropping the row), and chained `WITH` steps build several aggregates. This _Application policy reports, per venue, how many people will attend and how many playlists are approved. Note the identity is pinned in the `filter` here; you can equally pin it inline, e.g. `MATCH (subject:Person) WHERE subject.external_id = $token.sub`. MATCH (subject:_Application) MATCH (venue:Venue) OPTIONAL MATCH (venue)<-[:WILL_ATTEND]-(attendee:Person) WITH subject, venue, COUNT(attendee) AS attendeeCount OPTIONAL MATCH (playlist:Playlist)-[:APPROVED_FOR]->(venue) WITH subject, venue, attendeeCount, COUNT(playlist) AS playlistCount "filter": [ { "operator": "AND", "operands": [ { "operator": "=", "attribute": "subject.external_id", "value": "$_appId" }, { "operator": "=", "attribute": "venue.property.name", "value": "$venue_name" } ]} ] // knowledge query - venue facts plus the two computed counts { "nodes": ["venue.property.name", "venue.property.description"], "aggregate_values": ["attendeeCount", "playlistCount"] } // execution { "id": "kq-app-venue-attendance-stats", "input_params": { "venue_name": "Shower-Concert-Hall" } } # What can a filter express? A `filter` is a tree of conditions, used in the policy `condition` and, optionally, in the Knowledge Query. A **leaf** has an `attribute`, an `operator`, and (for most operators) a `value`. A **branch** uses `AND` / `OR` / `NOT` with nested `operands` and no `attribute`/`value`. ### Operators **Operator** **Meaning** `AND` / `OR`Combine nested `operands` (1 or more). `NOT`Negate exactly one nested operand. `=` / `<>`Equal / not equal. `>` / `<` / `>=` / `<=`Numeric or datetime comparison. `IN`Attribute is in the supplied array `value`. `=~`Attribute matches a regular-expression `value`. `STARTS WITH` / `ENDS WITH`String prefix / suffix match. `CONTAINS`The `value` is present as a whitespace-separated token within the attribute's string (membership in a space-separated list, e.g. OAuth `scope`). `IS NULL` / `IS NOT NULL`Attribute is absent/null or present/non-null. No `value`. ### Value forms - **Raw scalar** - string, number, or boolean (e.g. `0.8`, `"Upbeat"`, `true`). - **Array** - for `IN` (e.g. `["track-1", "track-2"]`). - **`$param`** - an execution-time parameter supplied in `input_params` (string values 1-256 chars). - **`$token.`** - a claim from the caller's token (`$token.sub`, `$token.acr`, `$token.iat`, `$token.scope`, ...). - **`$_appId`** - reserved; the calling Application's `external_id`, auto-filled for `_Application` subjects. - **Typed value object** - `{ "type": "datetime", "value": "2026-01-15T00:00:00Z" }` or `{ "type": "datetime", "value": "$control_date" }`. `type` defaults to `any`; use `datetime` for RFC 3339 timestamps so comparisons are date-aware. - **`@reference`** - a reference to *another matched attribute, by name*, instead of a constant. This is how you compare one property to another (see below). **Escaping.** The leading `$` and `@` are special. To use a literal string that actually starts with one of them, escape it: `\$rawValue` or `\@user`. Branches nest arbitrarily (`AND` of `OR`s, etc.). A **`token_filter`** is a sibling of `filter` that only references `$token.*` values and can attach step-up `advice` (`error`, `error_description`) returned via the `Www-Authenticate` header when the token is insufficient. See the ContX IQ guide for the full filter, typed-value, and `token_filter` schema. ### Comparing one property to another (`@` references) By default a filter compares an attribute to a *constant* (or a `$param` / `$token` value). To compare it to **another attribute matched by the Cypher** - a property-to-property comparison - prefix that operand with `@` and give the attribute's full name. It is the structured-filter equivalent of an inline `WHERE a.x <= b.y`. // a Track may PLAY at a Venue only if its loudness is within the venue's limit // cypher: MATCH (subject:Track)-[r:PLAYED_AT]->(venue:Venue) { "attribute": "subject.property.loudness", "operator": "<=", "value": "@venue.property.max_loudness" } The `@` name must resolve to a variable and attribute that the Cypher actually binds (any of the attribute forms above - `@venue.property.max_loudness`, `@other.external_id`, `@contract.trust_score._final_score`, `@subject.property.email.metadata.source`). **Version note.** Property-to-property comparison with `@` works in **both versions**; they differ only in which operand may hold the reference. In **v1**, only the `value` side may be an `@` reference (the `attribute` side is the property being tested). In **v2 (Beta)**, *both* the `attribute` and the `value` may be `@` references, so you can compare two referenced attributes directly. # Filter vs inline WHERE - which should I use? - **Use the `filter` array** for execution-time parameters (`$param`), token claims (`$token.*`), and the `$_appId` binding. It is the structured, validated way to express partial filters and supports nested `AND` / `OR` / `NOT`, typed values, and step-up `token_filter` advice. - **Use inline `WHERE`** for static, structural constraints inside the pattern - fixed thresholds, label predicates, list predicates (`ALL` / `ANY` / `NONE`), and relationship conditions that are part of the traversal logic. # Will my query time out? Execution enforces a per-query timeout. The default for read-only queries is **30 seconds**, tuned for fast, interactive queries; a read-only query that legitimately needs longer can set `batch_read: true` in the Knowledge Query to use the extended limit (5 minutes / 300 seconds - see batch_read). The right fix is almost always to make the matched subgraph smaller, not to wait longer. - **Anchor on an indexed identity.** Constrain the subject with `subject.external_id = $token.sub` (users) or `$_appId` (applications), or pin a node by `external_id` (inline `{external_id: $x}` or a filter). Traversal then starts from one node instead of a full label scan. Examples 1-6 and 8 all do this. - **Bound every variable-length path.** Write `[:INVOKES*1..5]`, never a bare `[:INVOKES*]` - an unbounded `*` can traverse the whole graph and loop on cycles, the most common cause of timeouts. This is the only traversal cost in example 7, which is why it bounds the depth. - **Filter to specific entities, not whole labels.** Prefer `playlist.external_id = $playlist_external_id` or `venue.property.name = $venue_name` over matching every `Playlist` or `Venue`. When a pattern does start from a label (e.g. `MATCH (venue:Venue)` in example 8), make sure a filter narrows it before any expansion or aggregation. - **Aggregate over bounded subgraphs.** `COUNT` / `COLLECT` are cheap over one anchored entity's neighbours (example 8 counts a single venue's attendees). Aggregating across an entire label is what warrants `batch_read`. - **Page large result sets.** Use `page_size` and `page_token` on the execute call to fetch results in chunks rather than one oversized response. Every example in this guide is anchored and bounded, so each returns quickly on a realistically sized graph. Reach for `batch_read` only when a read is intentionally broad. # Checklist: authoring a CIQ Cypher Before submitting a policy, verify each of these. Most rejections at create time trace back to one of them. - **Exactly one subject.** The pattern binds a node to the variable `subject`, and its label matches `subject.type`. One subject type per policy - need two? Write two policies. - **Name everything you reference.** Every node/relationship used in `filter`, `allowed_*`, or the Knowledge Query must have a Cypher variable. Anonymous nodes (e.g. `(:Track)`) can only connect the pattern. - **No `RETURN`, `CREATE`, `MERGE`, `SET`, or `DELETE` in the Cypher.** Results come from the Knowledge Query; mutations from `upsert_*` / `delete_*` gated by `allowed_*`. - **No `USE` clauses or `CALL { }` subqueries.** Database-routing and subquery syntax is rejected at create time for every CIQ policy version and for `2.0-kbac`: a `USE` clause fails with `USE clause is not allowed`, a `CALL { }` subquery with `CALL { } subquery is not allowed` (in KBAC policies the `CALL` keyword is caught by the token filter first: `Cypher contains forbidden clauses: [CALL]`). `use` remains valid as an ordinary variable, label, or property name. The one exception is the raw-Cypher `3.0-kbac` KBAC version, where `USE graph.byName(...)` and `CALL { }` subqueries (with inner `RETURN`s) are legal composite-database routing - see the data residency guide. This does not apply to CIQ. - **Allow-list every read.** Each variable/attribute the query returns must appear in `allowed_reads` (and aggregates in `allowed_reads.aggregate_values`). - **Parameters line up.** Each `$x` used must be supplied in `input_params` at execution (string values 1-256 chars). Do *not* send `$_appId` for `_Application` subjects - it is auto-filled. Bind the identity (`subject.external_id = $token.sub` for users) so a query cannot resolve other people's data. - **Expose aggregates via `WITH ... AS alias`** and list the alias in `aggregate_values`. - **Type your relationships for writes.** If a named relationship feeds `allowed_upserts`/`allowed_deletes`, give it an explicit type in the Cypher (e.g. `[r:PLAYED_AT]`, not `[r]`). Required in v2, recommended in v1. - **System-managed elements are off-limits.** A Knowledge Query cannot create, update, or delete the reserved `_Application` identity node, and cannot create system-internal node types. Protected properties cannot be deleted: `_service`, `create_time`, `external_id`, `id`, `type`, `update_time`. - **Keep it inside the timeout.** Anchor on an indexed identity, bound variable-length paths (`*1..N`), and filter to specific entities so the matched subgraph stays small. Set `batch_read: true` only for intentionally large reads. See Will my query time out? - **Validate by creating it.** Create the policy and query through the public REST Config API - `POST /configs/v1/authorization-policies` and `POST /configs/v1/knowledge-queries` (or use Terraform). The API rejects syntax and reference errors immediately. Full reference: openapi.indykite.com. # Quick recipe: from requirement to policy - **State the access in one sentence** - "an Application can read tracks playable at a named venue", or "a Person can view a family member's playlist". - **Pick the subject** - `Person` (user token) or `_Application` (service). This sets `subject.type` and the auth model. - **Draw the path** - write the `MATCH` from `subject` to the target, naming each node/edge you will read or mutate; set directions (`->`, `<-`, `-`). - **Constrain it** - identity (`$token.sub` / `$_appId`) and selectors (`$param`) in the `filter` array; static thresholds inline in `WHERE`. - **Declare capabilities** - fill `allowed_reads` / `allowed_upserts` / `allowed_deletes` with the variables from step 3. - **Write the Knowledge Query** - choose `nodes` / `relationships` / `aggregate_values` to return, or `upsert_*` / `delete_*` to mutate. - **Execute** - `POST /contx-iq/v1/execute` with the query id/name and `input_params`. # Next Steps - CIQ component model and full policy/query/execution syntax: ContX IQ: Context-Aware Data Queries and Policies - Cypher and graph traversal basics: Why use a Graph Database? - Fetch external data during a query: External Data Resolver - Use trust signals in conditions: Trust Score - Author from your AI agent: MCP Server · IndyKite from your AI coding agent - REST reference: Config API · Runtime API --- Source: https://developer.indykite.com/guides/guide-ciq-cypher --- # ContX IQ: Context-Aware Data Queries and Policies > Context-aware data queries: CIQ policies, Knowledge Queries and Execution. **Category:** ContX IQ ## Summary How to configure context-aware queries with CIQ policies and Knowledge Queries. ## Content # What is ContX IQ (CIQ)? ContX IQ (Contextual Intelligence Query) is IndyKite's **context-aware** data retrieval and mutation system. It delivers **the right data, at the right time, in the right context**. ### Why is CIQ context-aware? - **Query-time evaluation**: Every query is evaluated against the current state of the graph. - **Caller context**: Authorization considers the caller's identity, relationships, and parameters. - **Current state**: Results reflect the graph as it exists at query time. - **External integration**: Can fetch data from external APIs via External Data Resolver. CIQ consists of three components: - **CIQ Policy**: Defines what data can be accessed and what operations are allowed. - **CIQ Knowledge Query**: Specifies what to do with the data (read, create, update, delete). - **CIQ Execution Query**: Runs the knowledge query at runtime with specific parameters. # How do CIQ components work together? Think of it this way: - **Policy** = What's required (graph structure) + What's allowed (permissions) - **Knowledge Query** = What to do (read/create/update/delete operations) - **Execution Query** = Run it now with these parameter values **Schema example** # What credentials do I need? - **Creating policies and knowledge queries**: Service Account credentials (Config API) - **Executing queries**: AppAgent credentials + optional user access token Configuration methods: - Terraform: indykite_authorization_policy and indykite_knowledge_query resources - REST API: Config API documentation # CIQ Policy CIQ v2 - 2.0 (Beta testing only) at the bottom of this guide for what differs. --> ## What is a CIQ Policy? A CIQ Policy defines the authorization rules for data access. An administrator selects: - **Nodes and relationships**: The graph elements involved. - **Subject node**: One node must be designated as the subject (the entity requesting access). - **Filters**: Static or partial filters to constrain the data. ### What are static vs partial filters? **Static filter**: Value is hardcoded in the policy. Example: `group.property.status = 'active'` **Partial filter**: Value is provided at execution time. Example: `user.property.email = $user_email` ### Can I have multiple subject types? No. A policy is restricted to a single subject type. If you need two subjects (e.g., `Person` and `_Application`), create two separate policies. ### What is the _Application subject? When you create an Application with credentials, IndyKite creates `_Application` and `_AppAgent` nodes in the IKG. You can use `_Application` as an authenticated subject. When the subject type is `_Application`, add a filter with: - `attribute: subject.external_id` - `value: $_appId` (reserved value that matches the application's external_id) ## CIQ Policy Syntax { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "" }, "condition": { "cypher": "", "filter": [{ "operator": "", "attribute": "", "value": "", "operands": [{ "operator": "", "attribute": "", "value": "", "operands": [{...}] }] }], "token_filter": { "operator": "", "attribute": "", "value": "", "operands": [{ "operator": "", "attribute": "", "value": "", "operands": [{...}], "advice": { "error": "", "error_description": "" } }], "advice": { "error": "", "error_description": "" } } }, "allowed_upserts": { "nodes": { "existing_nodes": [""], "node_types": [""] }, "relationships": { "existing_relationships": [""], "relationship_types": [{ "type": "", "source_node_label": "", "target_node_label": "" }] } }, "allowed_deletes": { "nodes": [""], "relationships": [""] }, "allowed_reads": { "nodes": [""], "relationships": [""], "aggregate_values": [""] } } ### What does each field mean? meta - `policy_version`: The policy syntax version. Current version is `1.0-ciq`. subject - `type`: The node type for the subject (e.g., `Person`, `_Application`). condition `cypher`: The Cypher query defining nodes and relationships. Supports: - `MATCH`, `OPTIONAL MATCH`, `WHERE`, `WITH` clauses - Aggregate functions (e.g., `COLLECT({username: usernameProp.value}) AS usernames`) - Inline property filters (e.g., `MATCH (user:User {id: 1234})`) Each node/relationship must have a variable name to be referenced later. filter Array of filters for nodes and relationships. Each filter has: - `operator`: The comparison operator (see table below) - `attribute`: The attribute to compare (omit for AND/OR/NOT) `value`: The comparison value (omit for AND/OR/NOT). Can be: - a hardcoded constant; - a parameter or token reference with the `$` prefix (`$param`, `$token.`, `$_appId`); - a reference to another matched attribute with the `@` prefix (e.g. `@venue.property.max_loudness`) - this compares the `attribute` to another node/relationship property by name (property-to-property comparison). The `@` name must resolve to a variable and attribute bound by `condition.cypher`; - a **typed value** object for non-string types - see below. To use a literal string that begins with `$` or `@`, escape it as `\$` or `\@`. - `operands`: Array of nested filters (for AND/OR/NOT). Omit if empty. Typed filter values A filter `value` may be a raw scalar (default behavior) *or* an explicit typed object: { "attribute": "ln.property.registered_since", "operator": ">", "value": { "type": "datetime", "value": "$control_date" } } Supported types: - `any` (default): The value is used as-is. - `datetime`: The value must be an RFC 3339 timestamp string (e.g., `"2026-01-15T00:00:00Z"`) or a parameter reference (`$control_date`). Plain `value: "..."` (without a wrapping object) is still accepted and treated as `any`. What operators are supported? **Operator** **Description** **Operands required** `NOT` Negates the nested filter Exactly 1 `AND` Conjunction of nested filters 1 or more `OR` Disjunction of nested filters 1 or more `=` Attribute equals value - `<>` Attribute not equal to value - `>` Attribute greater than value - `<` Attribute less than value - `>=` Attribute greater than or equal to value - `<=` Attribute less than or equal to value - `IN` Attribute is in value array - `=~` Attribute matches regex pattern - `STARTS WITH` Attribute starts with value - `ENDS WITH` Attribute ends with value - `IS NULL` Attribute is not present or NULL - `IS NOT NULL` Attribute is present and not NULL - `CONTAINS` Value is present as a whitespace-separated token within the attribute's string (membership in a space-separated list, e.g. an OAuth `scope` claim) - What attribute naming conventions are used? **Pattern** **Description** **Example** `$token.` Property on the requestor token `$token.acr` `.` Attribute on a node or relationship `user.external_id` `.property.` Property on a node `user.property.email` `.property..metadata.` Metadata on a property `user.property.email.metadata.source` token_filter Similar to `filter`, but only works with `$token` values. Includes: `advice`: Step-up advice when filter is not satisfied. - `error`: Error name for the filter. - `error_description`: Description of the error. Returned in the response with `Www-Authenticate` header as "insufficient_user_authentication". allowed_upserts Defines what nodes and relationships derived queries can create or update. - `nodes.existing_nodes`: Node variables from `cypher` that can be updated. Must already exist in IKG. - `nodes.node_types`: Node labels that can be created as new nodes. - `relationships.existing_relationships`: Relationship variables from `cypher` that can be updated. `relationships.relationship_types`: Relationship types that can be created: - `type`: Relationship type - `source_node_label`: Source node label - `target_node_label`: Target node label Omit `allowed_upserts` or any sub-field if empty. allowed_deletes Defines what nodes and relationships derived queries can delete. - `nodes`: Node variables that can be deleted (e.g., `person`, `person.*` for wildcard). - `relationships`: Relationship variables that can be deleted (e.g., `r1`, `r1.*` for wildcard). Omit `allowed_deletes` if empty. allowed_reads Defines what data derived queries can return as results. - `nodes`: Node variables that can be returned (e.g., `subject`, `subject.*`, `person.property.email`). - `relationships`: Relationship variables that can be returned. - `aggregate_values`: Variables from aggregate functions in `cypher`. Example: "allowed_reads": { "nodes": ["subject", "subject.*", "person", "person.*"] } This allows the Knowledge Query to use: `subject`, `person`, `subject.property.email`, `person.property.name`, etc. # CIQ Knowledge Query ## What is a Knowledge Query? A Knowledge Query specifies what operations to perform on the data. It references a policy and defines: - What data to read - What nodes/relationships to create or update - What nodes/relationships to delete The query and policy are compiled into Cypher code for execution. ## CIQ Knowledge Query Syntax { "filter": { "operator": "", "attribute": "", "value": "", "operands": [{ "operator": "", "attribute": "", "value": "", "operands": [{...}] }] }, "upsert_nodes": [{ "name": "", "type": "", "external_id": "", "labels": [""], "is_identity": , "properties": [{ "type": "", "value": "", "metadata": [{ "type": "", "value": "" }] }] }], "upsert_relationships": [{ "name": "", "source": "", "target": "", "type": "", "properties": [{ "type": "", "value": "", "metadata": [{ "type": "", "value": "" }] }] }], "delete_nodes": [""], "delete_relationships": [""], "nodes": [""], "relationships": [""], "aggregate_values": [""], "batch_read": true } ### What does each field mean? filter Additional filters for the query. Same structure as policy filter. Omit if empty. upsert_nodes Array of nodes to create or update. Omit if empty. `name`: Variable name for the node. - Updating existing node: Use variable name from policy `cypher`. - Creating new node: Use a distinct name not used elsewhere. - `type`: Node label. - `external_id`: External ID for the node. Required for new nodes, omit for updates. Can be parameterized with `$` prefix. `labels`: Optional array of additional labels attached alongside `type`. - **Identity nodes**: `"labels": ["DigitalTwin"]` creates the node as an identity node - equivalent to setting `is_identity: true` (below, or its Capture API namesake), which is shorthand for adding this label. One of the two - the label or the flag - is required whenever the node will act as a `2.0-kbac` authorization subject; a non-identity subject makes every `2.0-kbac` decision silently `false` (`3.0-kbac` matches by type and external ID and does not need it). - Labels are not checked against the policy's `allowed_upserts.nodes.node_types` whitelist - only `type` is. - Label values are not validated either: a typo such as `Digitaltwin` is accepted silently and only surfaces when `2.0-kbac` decisions come back `false`. After creating, verify with a `2.0-kbac` evaluation that uses the new node as subject. - CIQ cannot bootstrap its own subject: the execute call authorizes against an existing subject node, so the first identity in a flow - the one the request runs as - must be ingested through the Capture API (`is_identity: true`). - `is_identity`: Optional boolean, default `false`. When `true`, the node is upserted as an **identity node** - the same result as `is_identity: true` in the Capture API and as adding `"labels": ["DigitalTwin"]` (both remain valid; combining them is harmless). Unlike the label, the boolean cannot be mistyped, so prefer it when marking identity nodes. `properties`: Array of properties to set. - `type`: Property name (must be hardcoded). - `value`: Property value (hardcoded or parameterized with `$`). `metadata`: Array of metadata for the property. - `type`: Metadata name (must be hardcoded). - `value`: Metadata value (hardcoded or parameterized). upsert_relationships Array of relationships to create or update. Omit if empty. `name`: Variable name for the relationship. - Updating existing: Use variable name from policy `cypher`. - Creating new: Use a distinct name. - `source`: Source node variable name. Omit if updating existing relationship. - `target`: Target node variable name. Omit if updating existing relationship. - `type`: Relationship type. Omit if updating existing relationship. - `properties`: Array of properties (same structure as node properties). delete_nodes Array of node variable names to delete (e.g., `car`, `car.property.model`). Must match variables in policy `cypher`. delete_relationships Array of relationship variable names to delete (e.g., `r1`, `r1.status`). Must match variables in policy `cypher`. **Protected properties** (cannot be deleted): `_service`, `create_time`, `external_id`, `id`, `type`, `update_time`. nodes Array of node variable names to return as results. Must match variables in policy `cypher` or `name`s in `upsert_nodes`. relationships Array of relationship variable names to return as results. Must match variables in policy `cypher` or `name`s in `upsert_relationships`. aggregate_values Array of aggregate variables (from policy `cypher` aggregate functions) to return as results. batch_read Optional boolean to enable batch mode for read queries. - **Default**: `false` - read queries run with a 30-second timeout. - **When `true`**: Increases query timeout to 5 minutes (300 seconds). **When should I use batch_read?** - Use when your query returns a large dataset that may exceed the default 30-second timeout. - Use for complex graph traversals that require more processing time. - Do not use for simple, fast queries where the overhead is unnecessary. # CIQ Execution Query ## What is an Execution Query? An Execution Query runs a Knowledge Query at runtime. It provides the parameter values for partial filters defined in the policy and query. ## How do I execute a CIQ query? **Endpoint:** `POST /contx-iq/v1/execute` **Authentication:** - Header: `X-IK-ClientKey: ` - If subject type is NOT `_Application`: Also include a third-party bearer token. ### What about _Application subjects? When the subject is `_Application`: - The `$_appId` input is automatically assigned from the Application node's `external_id`. - You don't need to provide it in `input_params`. ## Request format { "id": "knowledge_query_gid", "input_params": { "subject_external_id": "subject_external_id_value", "token_sub": "token_sub_value", "license_number": "license_number_value" }, "page_token": 0, "page_size": 100 } - `id`: The GID or name of the stored Knowledge Query. - `input_params`: Key-value pairs for each partial filter variable (without the `$` prefix). String values must be 1–256 characters. - `page_token` (optional): Page index for the result set. Any value below 1 returns the first page. - `page_size` (optional): Result set size. Default is `100`. ## Response format { "data": [ { "nodes": { "company.external_id": "companyParking", "payment.external_id": "cb123", "subject.external_id": "subject_external_id_value" }, "relationships": { "r1.external_id": "rel-abc" }, "aggregate_values": { "usernames": [{"username": "alice"}, {"username": "bob"}] } } ] } Each record in `data` may contain: - `nodes`: Values for the variables listed in the Knowledge Query `nodes`. - `relationships`: Values for the variables listed in the Knowledge Query `relationships`. - `aggregate_values`: Values for the variables listed in the Knowledge Query `aggregate_values` (results of Cypher aggregate functions such as `COLLECT`). Fields are omitted when empty. Every response also carries an `X-Indykite-Requestid` header - include it in support tickets to help trace a specific call. ### Response shape depends on what the query reads, not on read vs write vs delete The response is driven by whether the Knowledge Query requests any read output (`nodes`, `relationships`, or `aggregate_values`): - **Reads anything** (a read query, or an upsert/delete that also lists read fields - e.g. echoing back `["track.external_id"]` after a write): each `data` record contains the requested `nodes` / `relationships` / `aggregate_values`, as above. - **Reads nothing** (a pure write or delete with empty `nodes`, `relationships`, and `aggregate_values`): the response is a single **count** record - the number of matched rows the operation applied to - under the reserved `result` key: { "data": [ { "aggregate_values": { "result": 3 } } ] } So an upsert or delete that should return data must list the variables to read back; otherwise it returns only the count. This behavior is the same in both policy versions. - **Endpoint**: `POST /contx-iq/v1/execute` - there is no `/v2` path. The runtime dispatches on `meta.policy_version` inside the policy. - **Authentication**: `X-IK-ClientKey` (AppAgent), plus a third-party bearer token when the subject is not `_Application`. - **Request and response envelope**: identical shape (`id`, `input_params`, `page_token`, `page_size` → `data[]` with `nodes` / `relationships` / `aggregate_values`). - **Knowledge Query syntax**: identical fields and semantics. - **Policy syntax**: same shape; only the validation rules below differ. ## What differs in v2 ### Policy version A v2 policy sets the v2 identifier in `meta.policy_version` (provided when your project is opted in). v1 and v2 policies can coexist in the same project once v2 is enabled. ### Stricter validation: no untyped nodes or relationships in write operations If a node or relationship variable in `condition.cypher` has no explicit label/type (e.g., `(friend)` instead of `(friend:Person)`, or `(a)-[r]->(b)` instead of `(a)-[r:OWNS]->(b)`), v2 rejects that variable when it is used in a write or delete section: - `allowed_upserts.nodes.existing_nodes` / `allowed_upserts.relationships.existing_relationships` - `allowed_deletes.nodes` / `allowed_deletes.relationships` Validation error, e.g.: `untyped relationship cannot be used in
`. v1 silently allowed this. Fix in v2 by giving the node a label and the relationship a type in `condition.cypher`. Untyped nodes/relationships are still fine in read-only sections. ### Stricter validation: no references to anonymous nodes v2 rejects references in policy `filter` and Knowledge Query fields (`nodes`, `filter`, `upsert_*`, `delete_*`) that point to anonymous nodes in `condition.cypher` - that is, nodes with no variable name (e.g., the `(:Car)` in `MATCH (subject:Person)-[:OWNS]->(:Car)`). If you need to reference a node, give it a variable: `MATCH (subject:Person)-[:OWNS]->(car:Car)`. v1 did not catch these references at validation time. ### Filter extension: `@` references on the attribute side Both versions support property-to-property comparison by prefixing a filter operand with `@` to reference another matched attribute (e.g. `"value": "@venue.property.max_loudness"` - see the `value` field above). The versions differ only in **which operand** may hold the reference: - **v1**: only the `value` may be an `@` reference; the `attribute` is the property being tested. - **v2**: *both* the `attribute` and the `value` may be `@` references, so two referenced attributes can be compared directly. In either version, an unescaped leading `$`/`@` is treated as a parameter/reference; escape a literal as `\$`/`\@`. ## When should I use v2? - You are prototyping against the upcoming stable v2 dialect and want early validation feedback (anonymous-node and untyped node/relationship checks catch mistakes that v1 lets through), or you need `@` references on the filter `attribute` side. - You want to coordinate a project's policies against a known dialect for forward compatibility. Otherwise, stay on v1 - it is the stable, documented baseline. Both dialects share the same external API contract. ## How do I enable v2? v2 is gated per-project. Reach out to IndyKite support to have it enabled for your project, then set the v2 identifier (provided on opt-in) in your policy's `meta.policy_version`. --> # Next Steps - Full examples: Developer Hub Resources - External Data Resolver: Fetch data from external APIs during query execution - Trust Score: Assess data quality and use in authorization - Terraform provider: IndyKite Terraform Provider - REST API reference: Config API - Credentials guide: Credentials Guide --- Source: https://developer.indykite.com/guides/guide-contx-iq --- # Credentials, Tokens and API URLs > What credentials, tokens and URLs are needed for the IndyKite platform. **Category:** Environment ## Summary Acquiring Credentials and Access Tokens for API access. ## Content This guide explains the authentication components required to access the IndyKite platform. Use this reference to determine which credentials you need and how to configure them. ## What credentials do I need? The type of credentials depends on which API you want to call: - **Config API** (create configurations, projects, applications, policies, knowledge queries) → Use **Service Account credentials** - **All other APIs** (Capture, CIQ execution, AuthZEN evaluation, EntityMatching) → Use **AppAgent credentials** - **User-context operations** (KBAC queries, CIQ with user identity) → Use **User Access Token** with **AppAgent credentials** # Service Account Credentials ### What are Service Account credentials? Service Account (SA) credentials authenticate requests to the **Config API**. They are organization-level credentials used to manage configurations, create projects, and set up applications. The SA credentials file contains: - API endpoint URL - JWK (JSON Web Key) information - Bearer token for authentication ### How do I create Service Account credentials? **Option 1: Via the Hub UI (first time)** - Go to the IndyKite Hub at the Organization level. - Create a new Service Account. - Create credentials for the Service Account. - Download the credentials JSON file. **Option 2: Via REST API (subsequent credentials)** Endpoint: POST /service-accounts **Option 3: Via Terraform (subsequent credentials)** Plugin: Terraform plugin registry ### How do I use Service Account credentials with the REST API? - Open your SA credentials file and locate the `token` field. - In your HTTP request, set the Authorization header: `Authorization: Bearer ` Config API Reference: https://openapi.indykite.com/api-documentation-config ### How do I use Service Account credentials with Terraform? Set one of these environment variables: `export INDYKITE_SERVICE_ACCOUNT_CREDENTIALS_FILE=/path/to/credentials.json` Or provide the credentials content directly: `export INDYKITE_SERVICE_ACCOUNT_CREDENTIALS='{"serviceAccountId":"...","endpoint":"..."}'` Terraform Guide: https://developer.indykite.com/guides/guide-terraform # AppAgent Credentials ### What are AppAgent credentials? AppAgent credentials authenticate requests to all IndyKite APIs **except** the Config API: - Capture API (store nodes and relationships) - CIQ execute (read query and update graph data) - Authorization API (KBAC/AuthZEN policy evaluation) - EntityMatching API (identity resolution) AppAgent credentials are project-level credentials tied to a specific Application. ### How do I create AppAgent credentials? You must create resources in this order: - Create an **Application** under your Project. - Create an **Application Agent** under the Application. - Create **Credentials** for the Application Agent. **Methods:** - Hub UI: Navigate to your Project and create through the interface. - REST API: Environment setup example - Terraform: Terraform guide ### How do I use AppAgent credentials with the REST API? **Current method (API Key):** Set the header: `X-IK-ClientKey: ` REST API Reference: https://openapi.indykite.com/ ### What is the _Application node? When you create an Application with credentials, IndyKite automatically creates an `_Application` node in your Identity Knowledge Graph (IKG). You can use this node as an authenticated subject in CIQ queries, allowing your application to act as an identity in the graph. CIQ Guide: https://developer.indykite.com/guides/guide-contx-iq # User Access Tokens ### What is a User Access Token? A User Access Token is an OAuth 2.0 token issued by an external Authorization Server (identity provider) that represents a user's identity and permissions. The token contains: - **Claims**: Information about the user (email, name, roles). - **Scopes**: Permissions granted to the token (read, write, delete). - **Expiration**: Time limit for token validity. ### When do I need a User Access Token? Use User Access Tokens when you need to: - Execute KBAC/AuthZEN authorization queries on behalf of a user. - Run CIQ queries with user-specific context and permissions. - Enforce fine-grained access control based on user identity. ### How do I use a User Access Token? Include the token in your HTTP request header: `Authorization: Bearer ` Examples: Developer Hub Resources ### How does IndyKite validate User Access Tokens? IndyKite uses **Token Introspection** to validate tokens and extract user claims. You must configure a Token Introspect policy that tells IndyKite: - Which claim to use for matching (e.g., email, sub). - Which node type to match against in the IKG (e.g., Person). Token Introspect Guide: https://developer.indykite.com/guides/guide-token-introspect REST Configuration: POST /token-introspects # API URLs ### Which API URL should I use? Choose the URL based on your data residency requirements: - **EU Region**: `https://eu.api.indykite.com` - **US Region**: `https://us.api.indykite.com` Use the same region as your IndyKite Hub instance. # Security Best Practices ### How should I store credentials securely? - **Never** commit credentials to version control (Git, GitHub). - **Never** expose credentials in client-side code or browser JavaScript. - **Never** log credentials or tokens in application logs. **Recommended storage methods:** - Environment variables (for local development). - Secret management services (AWS Secrets Manager, HashiCorp Vault, Azure Key Vault). - CI/CD secret storage (GitHub Secrets, GitLab CI Variables). ### What should I do if credentials are compromised? - Immediately revoke the compromised credentials in the IndyKite Hub. - Generate new credentials. - Update all applications using the old credentials. - Review access logs for unauthorized activity. --- Source: https://developer.indykite.com/guides/guide-credentials --- # Data Residency & Composite Databases > Control where your Identity Knowledge Graph data lives: regions, replicas, routing data to specific locations with a composite database, and location-aware authorization with 3.0-kbac policies. **Category:** Environment ## Summary Choose the region your IKG runs in, add a read replica in a second region, route individual nodes to specific locations with a customer-hosted composite database, and run location-aware AuthZEN authorization with 3.0-kbac policies. ## Content # What is Data Residency? Data residency controls **where your Identity Knowledge Graph (IKG) physically lives**. Every IndyKite project is created in a region, and that region determines where your graph data is stored and processed. This matters when your data is subject to regulatory requirements (for example GDPR or sector-specific rules that mandate in-region storage), or when you want the IKG close to your users for latency reasons. ### Which residency settings does a project have? **Field** **Description** **Values** `region` Region where the project's IKG is provisioned. Required at creation, cannot be changed afterwards. Depends on your environment, for example `europe-west1`, `us-east1` `ikg_size` Storage size of the IKG instance; each size comes with a corresponding CPU allocation. Depends on your environment, for example `2GB`, `4GB`, `8GB`, `16GB`, `32GB`, `64GB`, `128GB`, `192GB`, `256GB`, `384GB`, `512GB` `replica_region` Optional second region that receives a synchronized read replica of the IKG. Depends on your environment, for example `europe-west1`, `us-east1`, `us-west1` `ikg_status` Provisioning state of the IKG (read-only, returned on reads). `PENDING`, `ACTIVE`, `FAILED`, `PAUSED` ### How do replicas work? Setting `replica_region` provisions a **second IKG in another region** that is continuously populated from the primary by change-data-capture replication. - The replica must be in a **different region than the primary**, but on the **same geographical continent**. - The replica is **read-only**: it is served through a read-only database user and cannot accept writes. - Replication is one-way, from the primary region to the replica region. - Use it for in-region read access (queries, authorization decisions) close to users in a second geography. # What is a Composite Database? A region places your *whole* IKG in one location. A **composite database** goes further: it lets **one logical IKG span multiple physical databases**, so that individual nodes can be stored in a specific location: for example, EU customer records in an EU database and US customer records in a US database, while your applications keep talking to a single graph. A composite IKG is made of **constituent databases**: **Constituent** **What it stores** **Global database** Lightweight *proxy nodes* (external ID, type, and location only: no property data) for every located node, plus the relationships that connect nodes across locations. **Location databases** The full node data for each logical location (for example `east`, `west`). **Default database** Nodes ingested *without* a location: they follow the same path as a regular, non-composite IKG. Two settings on the project's database connection wire this together: - `composite_db_name`: the name of the composite database. Leave it empty for a regular single-database IKG. - `alias_mapping`: a URL-query-encoded map from **logical location names to constituent databases**, for example `global=db1&east=db2&west=db3`. The location names on the left are the values you will later put in the `location` field of Capture API requests. ### When should I use a composite database? - You must keep certain nodes' data in a specific jurisdiction, but still need one connected graph across all of them (cross-location relationships, single authorization model). - You want per-record placement decisions (this person's data stays in the EU) rather than a per-project region choice. **Availability:** composite databases are supported for **customer-hosted** IKGs only: you run your own Neo4j deployment and IndyKite connects to it. IndyKite-managed projects use a regular single-database IKG in the project's region. # How do I create a project with residency settings? Residency is set on the Config API when creating a project. Authenticate with your Service Account token (see the credentials guide). POST /configs/v1/projects { "name": "my-project", "display_name": "My Project", "organization_id": "gid:your-organization-id", "region": "us-east1", "ikg_size": "2GB", "replica_region": "us-west1" } With header `Authorization: Bearer `. Provisioning is **asynchronous**: the response returns immediately, while the IKG is created in the background. Poll the project until it is ready: GET /configs/v1/projects/{id} { "id": "gid:your-project-id", "region": "us-east1", "ikg_size": "2GB", "replica_region": "us-west1", "ikg_status": "ACTIVE" } Wait for `ikg_status` to become `ACTIVE` before ingesting data. `PENDING` means provisioning is still running; `FAILED` means it did not complete. # How do I set up a composite database? ### Step 1: Create the databases in your Neo4j deployment The composite database and its constituents must exist **before** you create the project. IndyKite connects to them and configures routing; it does not create the databases for you. On your Neo4j instance: CREATE DATABASE db1 IF NOT EXISTS; CREATE DATABASE db2 IF NOT EXISTS; CREATE DATABASE db3 IF NOT EXISTS; CREATE COMPOSITE DATABASE ikcomposite IF NOT EXISTS; CREATE ALIAS ikcomposite.db1 FOR DATABASE db1; CREATE ALIAS ikcomposite.db2 FOR DATABASE db2; CREATE ALIAS ikcomposite.db3 FOR DATABASE db3; In a real deployment the constituent databases typically live on servers in different locations; the composite database presents them as one graph. ### Step 2: Create the project with the composite connection POST /configs/v1/projects { "name": "my-project", "display_name": "My Project", "organization_id": "gid:your-organization-id", "region": "europe-west1", "db_connection": { "url": "neo4j://your-neo4j-host:7687", "username": "neo4j", "password": "", "name": "db1", "composite_db_name": "ikcomposite", "alias_mapping": "global=db1&east=db2&west=db3" } } **db_connection field** **Meaning** `url`, `username`, `password` Connection to your Neo4j deployment (required). `name` The default constituent database: used for operations that carry no `location`. `composite_db_name` Name of the composite database created in Step 1. Empty means a regular IKG. `alias_mapping` Location-to-constituent map. By convention, map the `global` location to the constituent that should hold proxy nodes and cross-location relationships. ### Step 3: Wait for the IKG to become active As with any project, poll `GET /configs/v1/projects/{id}` until `ikg_status` is `ACTIVE`. The response echoes `db_connection.composite_db_name` and `db_connection.alias_mapping` so you can verify the configuration round-trips. ### Can I change the composite configuration of an existing project? Yes, with an update (subject to a few rules): PUT /configs/v1/projects/{id} { "db_connection": { "url": "neo4j://your-neo4j-host:7687", "username": "neo4j", "password": "", "name": "db1", "composite_db_name": "ikcomposite", "alias_mapping": "global=db1&east=db2&west=db3" } } - Optionally send an `If-Match: ""` header (from a previous read) to guard against concurrent updates. - `db_connection` can only be updated on **customer-hosted** projects; on IndyKite-managed projects it is rejected. Omitting `db_connection` leaves the existing connection untouched. - The databases referenced by the new mapping must already exist in Neo4j. The update rewires configuration and routing: it does **not** migrate existing data between constituent databases. - The hosting type itself cannot change: an IndyKite-managed project cannot be turned into a composite one. Composite requires customer-hosted from creation. - The update is applied asynchronously: poll `ikg_status` as for creation. # How do I ingest data with locations? On a composite IKG, the Capture API routes data by location. Authenticate with your Application Agent key: `X-IK-ClientKey: `. - **Nodes** are routed *per node* with an optional `location` field (2–32 characters, must be a key of the project's `alias_mapping`). - **Relationships** are routed *per request* with `"use_global_db": true`: relationships can connect nodes living in different locations, so they are stored in the global constituent alongside the proxy nodes. ### Upsert nodes with per-node locations POST /capture/v1/nodes { "nodes": [ { "external_id": "person-alice", "type": "Person", "is_identity": true, "location": "east", "properties": [ { "type": "name", "value": "Alice Marchetti" }, { "type": "email", "value": "alice@example.com" } ] }, { "external_id": "person-karel", "type": "Person", "is_identity": true, "location": "west", "properties": [ { "type": "name", "value": "Karel Plihal" } ] }, { "external_id": "car-kitt", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "Pontiac" } ] } ] } What happens: - The full `person-alice` node is stored in the `east` constituent, `person-karel` in `west`. - For every located node, a **proxy node** (external ID, type, and location (no properties)) is automatically upserted into the global constituent, so the nodes stay addressable graph-wide. - `car-kitt` has no `location`, so it goes to the default database, exactly as on a non-composite IKG. ### Upsert relationships into the global database POST /capture/v1/relationships { "use_global_db": true, "relationships": [ { "source": { "external_id": "person-alice", "type": "Person" }, "target": { "external_id": "car-kitt", "type": "Car" }, "type": "OWNS", "properties": [ { "type": "status", "value": "active" } ] } ] } The relationship is created in the global constituent between the proxy nodes, connecting entities whose full data lives in different locations. ### Which delete operations are location-aware? All Capture API delete endpoints accept the same routing fields: **Endpoint** **Routing field** `POST /capture/v1/nodes/delete` `location` per node `POST /capture/v1/nodes/properties/delete` `location` per node `POST /capture/v1/nodes/properties/metadata/delete` `location` per node `POST /capture/v1/relationships/delete` `use_global_db` per request `POST /capture/v1/relationships/properties/delete` `use_global_db` per request Example: deleting nodes across locations (deleting a located node removes both its data node in the location constituent and its proxy in the global database): POST /capture/v1/nodes/delete { "nodes": [ { "external_id": "person-alice", "type": "Person", "location": "east" }, { "external_id": "person-karel", "type": "Person", "location": "west" }, { "external_id": "car-kitt", "type": "Car" } ] } ### What happens if the location is wrong? **Situation** **Result** A `location` that is not a key in the project's `alias_mapping` Request fails: unknown location A `location` on a project with no composite database configured Request fails: composite database not configured No `location` at all Node goes to the default database (legacy behavior, always safe) # How do queries and authorization work on a composite IKG? Reads do **not** fan out across constituent databases by default. ContX IQ queries (`POST /contx-iq/v1/execute`) and KBAC / AuthZEN authorization checks with `2.0-kbac` policies (`POST /access/v1/*`) execute against the **default constituent database only**: the one named by `db_connection.name`. - **Point the default at the global constituent** (as in this guide's examples, where `name` is the database mapped to `global`). Authorization decisions that depend on graph *structure* keep working, because they traverse the proxy nodes and the cross-location relationships stored there. - **Located property data is out of read scope.** Proxy nodes carry only external ID, type, and location: the full properties stored in a location constituent (for example a person's email in `east`) are not returned by queries and cannot be referenced in policy conditions running against the default database. - **CIQ and `2.0-kbac` policy Cypher cannot route to a constituent.** A `USE` clause or a `CALL { }` subquery is rejected when the policy is created (see the CIQ Cypher guide). - **To make authorization decisions against a location constituent**, author the policy as `3.0-kbac`: the next section shows how. # How does authorization work with data residency? (3.0-kbac) The `3.0-kbac` policy version makes KBAC / AuthZEN authorization **location-aware**. Where the platform rewrites a `2.0-kbac` condition and always runs it against the default database, a `3.0-kbac` condition is **raw Cypher**: it runs as you wrote it, and you author the composite-database routing yourself with `USE graph.byName(...)`. Residency support is **opt-in per policy**: existing `2.0-kbac` policies are unchanged. Note that `3.0-kbac` itself does not require a composite database - only `USE` routing does; a `3.0-kbac` policy without a `USE` clause evaluates against the default database as plain raw Cypher. Routing can be: - **Static**: `USE graph.byName('ikcomposite.db2')`: always evaluates in that constituent. - **Dynamic**: `USE graph.byName($region)`: `$region` becomes a **location parameter**. The caller passes a *logical location* (a key of the project's `alias_mapping`, for example `"east"`) in the AuthZEN request's `context.input_params`, and IndyKite translates it to the physical constituent just before execution. Callers never see or supply physical database names. ### Example: a location-routed policy Create the policy on the Config API (`POST /configs/v1/authorization-policies`) like any other KBAC policy; only the policy JSON differs: { "meta": { "policy_version": "3.0-kbac" }, "subject": { "type": "Person" }, "actions": ["CAN_DRIVE"], "resource": { "type": "Car" }, "condition": { "cypher": "USE graph.byName($region) MATCH (subject:Person)-[:OWNS]->(resource:Car)" } } Then evaluate with a location in `context.input_params`: POST /access/v1/evaluation { "subject": { "type": "Person", "id": "person-alice" }, "action": { "name": "CAN_DRIVE" }, "resource": { "type": "Car", "id": "car-kitt" }, "context": { "input_params": { "region": "east" } } } `"east"` is the logical location from `alias_mapping`, not a Neo4j database name. The same pattern works on all AuthZEN endpoints: single evaluation, batch evaluations, and the three search endpoints. ### What can 3.0-kbac Cypher do that 2.0-kbac cannot? - `USE graph.byName(...)` clauses, top-level or per `CALL { }` subquery, so one condition can combine matches from several constituents. - `CALL { }` subqueries with inner `RETURN`s (both rejected in `2.0-kbac`). - Mutating clauses (`CREATE`, `MERGE`, `SET`, `DELETE`, and so on) remain blocked, as does a top-level `RETURN`: the platform appends the projection itself. ### Authoring rules for 3.0-kbac - The condition must still bind `subject` and `resource` variables; the platform pins them by type and external ID and replaces the final projection. - The `graph.byName()` argument must be a **string literal or a single parameter**: expressions like `coalesce($region, 'eu')` are rejected at creation. - A routing parameter **cannot be referenced anywhere else** in the Cypher: its value is rewritten to the physical alias at request time. - `$subject_external_id`, `$subject_type`, `$resource_external_id`, and `$resource_type` are bound by the platform: never supply them in `input_params` or use them as routing parameters. - `$subject_id` must not be referenced at all: on a composite IKG the same logical subject has a different internal node ID per location, so it is rejected at creation. - External (resolver-backed) properties cannot be used in the condition: creation fails with `external properties cannot be used in data-residency policies`. They are supported only in `2.0-kbac` conditions. - The subject does not need to be an identity node: `3.0-kbac` matches it by type and external ID (`2.0-kbac` requires a subject ingested with `is_identity: true`). With a bearer token on the request, the token's subject must match the requested subject. ### What happens if the location is wrong at decision time? **Situation** **Result** Required location parameter missing from `context.input_params`, or not a non-empty string `422 Unprocessable Entity` Location is not a key of the project's `alias_mapping` `422 Unprocessable Entity` (unknown location) Policy routes with `USE` (static or via a location parameter) but the project has no composite database `422 Unprocessable Entity` (requires a composite database) Bearer-token call where the token's subject differs from the requested subject `403 Forbidden` (permission denied) See the AuthZEN guide for the full request shape, and the `authz-7` / `authz-8` resources for end-to-end examples. # Related resources - Credentials guide: Service Account tokens (Config API) vs Application Agent keys (Capture API) - AuthZEN guide: request shape, batch evaluations, and location parameters - Why use a Graph Database?: IKG fundamentals - Config API reference and Capture API reference --- Source: https://developer.indykite.com/guides/guide-data-residency --- # IKG Data Schema: Nodes, Relationships and Properties > How data is shaped in the IndyKite Knowledge Graph - node and relationship structure, property values, metadata - and how to read your project's schema back through the Data Schema API. **Category:** Data Schema ## Summary Reference for the IKG data model - the node, relationship, property, and metadata shapes with their value types and limits - plus the Data Schema API (GET /data-schema/v1/) that returns your project's live schema in JGF v2 format. ## Content # What is the IKG data schema? The IndyKite Knowledge Graph (IKG) is a property graph. Everything you store is one of three things: - **Nodes**: entities such as `Person`, `Car`, `Contract`, or `Device`. - **Relationships**: directed, typed connections between two nodes, such as `OWNS` or `CAN_DRIVE`. - **Properties**: typed values attached to a node or a relationship, optionally carrying provenance metadata. There is no schema definition step: you do not declare node types or property names up front. The schema *emerges* from the data you ingest. What the platform does enforce is the **shape** of that data - field structure, value types, and size limits - at every write, whether the data arrives through the Capture API or a ContX IQ upsert. The platform keeps track of the schema that emerges - which node types exist, which properties they carry, how they are connected - and exposes it through the **Data Schema API**, so you can read your project's schema back at any time. This guide is the reference for the data shapes and rules, and for the Data Schema endpoint. # What is a node? A node represents one entity. Its JSON shape, as sent to `POST /capture/v1/nodes`: { "external_id": "millicent", "type": "Person", "is_identity": true, "labels": ["Customer"], "properties": [ { "type": "email", "value": "millicent@email.com" } ] } **Field** **Required** **Rules** **Meaning** `external_id` yes 1–256 characters Your identifier for the entity - a customer number, serial number, or any ID from your source system. `type` yes 2–64 characters. Convention: PascalCase. The node's kind, e.g. `Person`, `Car`. Becomes the graph label that policies and queries match on. `is_identity` no boolean `true` marks an identity node - a person or other actor that can appear as a subject in authorization decisions. Omit or `false` for plain resource nodes. `labels` no array of strings. Convention: PascalCase. Additional labels beyond `type` for cross-cutting classification. `properties` no array Typed values - see properties below. `location` no 2–32 characters; must be a key of the project's `alias_mapping` Composite IKG only: routes the node to a constituent database. See the Data Residency guide. ## How is a node identified? The pair **(`type`, `external_id`)** uniquely identifies a node. Writes are **upserts**: posting the same pair again updates the existing node instead of creating a duplicate. The same pair is how relationships, deletes, and policies reference a node. The platform also assigns every node a read-only, globally unique `id` with a `gid:` prefix, returned in API responses: `{ "results": [ { "id": "gid:AAAAFezwD2VFdEHnhBmHdGqTDLU" } ] }` You never set `id` yourself - your own reference is always the (`type`, `external_id`) pair. ## Identity nodes vs. resource nodes The `is_identity` flag splits the graph into two roles: - **Identity nodes** (`is_identity: true`): actors - people, service accounts, agents. These are the subjects of AuthZEN authorization decisions. - **Resource nodes** (default): everything else - the things being owned, accessed, or acted upon. # What is a relationship? A relationship is a directed, typed edge between two existing nodes, each referenced by its (`type`, `external_id`) pair. Its JSON shape, as sent to `POST /capture/v1/relationships`: { "source": { "external_id": "millicent", "type": "Person" }, "target": { "external_id": "kitt", "type": "Car" }, "type": "OWNS", "properties": [ { "type": "since", "value": "2024-01-15" } ] } **Field** **Required** **Rules** **Meaning** `source` yes `external_id` + `type` The node the relationship points *from*. `target` yes `external_id` + `type` The node the relationship points *to*. `type` yes max 128 characters. Convention: an uppercase verb in UPPER_SNAKE_CASE. The relationship's kind, e.g. `OWNS`, `CAN_DRIVE`, `BELONGS_TO`. `properties` no array Same property shape as on nodes, including metadata. ## Direction matters A relationship always runs from `source` to `target`. `Person -[OWNS]→ Car` and `Car -[OWNS]→ Person` are two different edges. Model in the direction that reads naturally as a sentence - queries and policies can still traverse edges in either direction, but the stored direction is part of the schema your Cypher patterns match against. A relationship is addressed by its **(source, target, type)** triple: writes are upserts on that triple, and the delete endpoint identifies the edge the same way. Like nodes, every relationship also receives a read-only platform-assigned `gid:` identifier, returned in the API response. # What is a property? A property is a named, typed value on a node or relationship: { "type": "email", "value": "millicent@email.com" } **Property names** (`type`) can be up to 128 characters. The convention is snake_case or camelCase, e.g. `given_name`, `startDate`. ## Which value types are supported? **Type** **JSON example** String `"value": "Millicent"` Integer `"value": 42` Float `"value": 4.5` Boolean `"value": true` Array `"value": ["red", "green"]` Timestamps are stored as RFC 3339 strings, e.g. `"2026-04-10T06:28:16Z"`. Keep each property a single value or a flat array; represent structured data as separate properties or - better - as separate nodes connected by relationships. If you find yourself nesting objects inside a property, that is usually a sign the object should be a node. ## Value or external value? A property carries either a stored `value` *or* an `external_value` - a reference to an External Data Resolver configuration that fetches the value from an external API at query time, so the data itself never lives in the IKG: { "type": "current_value", "external_value": { "name": "vehicle-pricing-resolver" } } # What is property metadata? Every property - on nodes and relationships alike - can carry a `metadata` object recording where the value came from and how much to trust it: { "type": "name", "value": "Millicent Contextsworth", "metadata": { "assurance_level": 3, "source": "BRREG", "verified_time": "2026-04-10T06:28:16Z", "custom_metadata": { "verification_method": "passport" } } } **Field** **Rules** **Meaning** `assurance_level` `1`, `2`, or `3` Confidence level of the value's verification - higher means stronger assurance. `source` string The system or authority the value came from, e.g. `"BRREG"`. `verified_time` RFC 3339 timestamp When the value was last verified. `custom_metadata` object Free-form key/value provenance of your own. Metadata is per-property, not per-node: two properties on the same node can come from different sources at different assurance levels. Trust Score profiles aggregate these fields into a queryable trustworthiness score. # Naming rules and limits at a glance **Element** **Limit / convention** **Examples** Node `type` 2–64 chars; PascalCase by convention `Person`, `PaymentMethod` Node `external_id` 1–256 chars `millicent`, `VIN-1982-KITT` Node `labels` strings; PascalCase by convention `Customer`, `DigitalTwin` Relationship `type` max 128 chars; uppercase verb by convention `OWNS`, `CAN_DRIVE` Property name max 128 chars; snake_case or camelCase by convention `email`, `given_name` Metadata `assurance_level` `1`, `2`, or `3` - `location` 2–32 chars; a key of the project's `alias_mapping`; composite IKG only `east`, `eu-west` Nodes / relationships per Capture request 1–250 - A request that violates a limit fails with `400 Bad Request` and an `errors[]` array naming the offending fields (e.g. a missing `external_id` or `type`, or a batch size outside 1–250). An unknown `location` (or any `location` on a non-composite project) fails with `422 Unprocessable Entity`. The authoritative field reference is the public OpenAPI specification. # A complete example A minimal vehicle-rental schema: one identity node, one resource node, one directed edge. **`POST /capture/v1/nodes`** { "nodes": [ { "external_id": "millicent", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "millicent@email.com" }, { "type": "name", "value": "Millicent Contextsworth", "metadata": { "assurance_level": 2, "source": "id-verification", "verified_time": "2026-04-10T06:28:16Z" } } ] }, { "external_id": "kitt", "type": "Car", "properties": [ { "type": "manufacturer", "value": "Pontiac" }, { "type": "seats", "value": 2 } ] } ] } **`POST /capture/v1/relationships`** { "relationships": [ { "source": { "external_id": "millicent", "type": "Person" }, "target": { "external_id": "kitt", "type": "Car" }, "type": "CAN_DRIVE", "properties": [ { "type": "valid_until", "value": "2027-01-01" } ] } ] } Resulting graph: `Person(millicent) -[CAN_DRIVE {valid_until}]-> Car(kitt)` From here the data is immediately usable: a KBAC policy can grant `Person` the `DRIVE` action on `Car` where the `CAN_DRIVE` edge exists, and a ContX IQ query can return every car a person can drive. # How do I read my project's data schema? The **Data Schema API** returns the schema the IKG has observed for your project - every node type, its properties with their value types, its labels, and every relationship type between node types - as a JSON Graph Format (JGF) v2 document. It is a schema-level view: types and occurrence counts, not the data itself. **Endpoint:** - EU: `GET https://eu.api.indykite.com/data-schema/v1/` - US: `GET https://us.api.indykite.com/data-schema/v1/` The request authenticates the calling application: pass the AppAgent credential in the `X-IK-ClientKey` header, as is, without any prefix (see the Credentials guide). The project is derived from the credential - there are no parameters. `curl -H "X-IK-ClientKey: $CLIENT_KEY" https://eu.api.indykite.com/data-schema/v1/` ## Response structure The response is one `graph` object: **Field** **Meaning** `graph.directed` Always a directed graph. `graph.metadata` `created_at` and `updated_at` timestamps of the schema. `graph.nodes` A map keyed by **node type**. Each entry's `metadata` holds `node_count` (how many nodes of the type exist), `properties` (a map keyed by property name - below), `system_labels`, and `user_defined_labels` (each a list of `{ name, count }`). `graph.edges` One entry per (source type, relation, target type) combination: `source` and `target` node types, the `relation` (relationship type), `directed`, and `metadata` with the edge `count` and its `properties` map. Each entry in a `properties` map describes one property: how many times it occurs (`count`), the observed value types with per-type counts (`types`), and - for node properties - a `metadata` map with the same statistics for the provenance fields attached to that property. ## Example After ingesting the vehicle-rental example above, the data schema describes it along these lines (illustrative response): { "graph": { "directed": true, "metadata": { "created_at": "2026-08-01T10:00:00Z", "updated_at": "2026-08-15T08:30:00Z" }, "nodes": { "Person": { "metadata": { "node_count": 1, "properties": { "email": { "count": 1, "types": [ { "type": "string", "count": 1 } ] }, "name": { "count": 1, "types": [ { "type": "string", "count": 1 } ] } } } }, "Car": { "metadata": { "node_count": 1, "properties": { "manufacturer": { "count": 1, "types": [ { "type": "string", "count": 1 } ] }, "seats": { "count": 1, "types": [ { "type": "integer", "count": 1 } ] } } } } }, "edges": [ { "source": "Person", "target": "Car", "relation": "CAN_DRIVE", "directed": true, "metadata": { "count": 1, "properties": { "valid_until": { "count": 1, "types": [ { "type": "string", "count": 1 } ] } } } } ] } } ## What can I use it for? The data schema is a description of what your graph actually contains - the exact type names, property names, observed value types, and counts - so its uses are all about knowing the graph's vocabulary and health without querying the data itself: - **Authoring policies and queries.** KBAC policies and ContX IQ Knowledge Queries reference node types, relationship types, and property names literally in Cypher. The schema gives you the exact spelling that exists in your project - so you don't write a policy against `givenName` when the data was ingested as `given_name`, or a traversal over a relationship that does not exist between those two types. - **Verifying ingests.** After a Capture batch or a new pipeline goes live, one GET confirms the expected types and properties landed, and the counts sanity-check the volume. A typo such as `"type": "Perosn"` shows up immediately as a surprise node type instead of hiding until a query returns nothing. - **Detecting schema drift and data-quality issues.** Each property lists its observed value types *with counts*: a property reporting `string: 4980, integer: 20` is a red flag that an upstream source started sending the wrong type. Polling the endpoint and diffing the response (`updated_at` tells you when the schema last changed) makes a cheap monitor for upstream changes. - **Feeding tools and AI agents.** Hand the JGF document to an LLM or agent before it writes ContX IQ queries or policies, and it knows the graph's vocabulary instead of guessing type names. The same applies to code generation - typed models, mapping configurations, or documentation built from the live schema. - **Visualization and onboarding.** The response is already a graph (JGF v2), so it renders directly as a meta-model diagram - "here is what our IKG looks like" for docs or new team members, without exposing any actual data. - **Impact analysis.** The per-property and per-edge counts tell you how much data a cleanup would touch before you call the property or relationship delete endpoints. What it is *not* for: it does not return the data itself (that is a ContX IQ read), and it does not *enforce* anything - the schema is descriptive, observed from what you have ingested, not a constraint definition you author. ## Errors **HTTP code** **When** `400 Bad Request` Malformed request; the body carries a `message`. `404 Not Found` Data schema not found for the specified Project - typically nothing has been ingested yet. The body carries `message` and `errors[]`. `500 Internal Server Error` Server-side issue; retry with backoff. # Schema design tips - **Prefer relationships over foreign-key properties.** Store `Person -[OWNS]-> Car`, not a `owner_id` property on the car - edges are what graph queries and KBAC policies traverse. - **Promote important values to nodes.** If a value is shared, matched on, or connected to more than one entity (a license number, an organization, an address), make it a node with its own relationships instead of a property. - **Keep `external_id` stable.** It is the upsert key - changing it creates a new node rather than renaming the old one. - **Name for the sentence.** `Person -[ACCEPTED]-> Contract -[COVERS]-> Vehicle` reads as the business rule it encodes; policies written against it stay legible. - **Attach provenance where decisions depend on it.** Any property that feeds an authorization decision or a Trust Score should carry `metadata`. # Next Steps - Why a graph in the first place: Why use a Graph Database? - Ingest walkthrough: Ingest Data into the IKG - Query the schema you built: ContX IQ guide - Authorize against it: AuthZEN guide - Route nodes across regions: Data Residency & Composite Databases - Resolve values from external APIs: External Data Resolver - REST API reference: Capture API (OpenAPI) and the Data Schema API specification --- Source: https://developer.indykite.com/guides/guide-data-schema --- # IndyKite Product Demos > Demos to configure the IndyKite platform and start using the different products. **Category:** Environment ## Summary What are the key components and steps for creating an environment in the IK platform and testing the products ## Content # What will I learn from these demos? These video demos provide step-by-step guidance for working with the IndyKite platform. They cover everything from initial setup to advanced features. ## Getting Started - **Environment setup**: Create your organization, projects, and applications in the IndyKite Hub. - **Credentials**: Generate Service Account and AppAgent credentials for API access. - **Identity Knowledge Graph (IKG)**: Understand how to connect your Neo4j database or use IndyKite's managed IKG. ## Data Capture - **Capturing nodes**: Learn how to store identity and resource nodes in your IKG. - **Capturing relationships**: Connect nodes with meaningful relationships. - **Data modeling**: Design your graph schema for authorization use cases. ## Authorization (KBAC) - **Creating policies**: Define Knowledge-Based Access Control (KBAC) policies. - **AuthZEN requests**: Make authorization decisions using the standard AuthZEN API. - **Testing policies**: Validate your policies with different subjects and resources. ## ContX IQ (CIQ) - **CIQ policies**: Create policies that authorize data operations. - **Knowledge queries**: Define queries to read, create, update, or delete graph data. - **Executing queries**: Run CIQ queries with dynamic parameters. ## Advanced Features - **Token Introspect**: Configure third-party token validation and identity mapping. - **Outbound Events**: Set up real-time event streaming to Kafka or Azure. - **Terraform**: Manage IndyKite configurations as infrastructure-as-code. # Who are these demos for? - **Developers**: Learn how to integrate IndyKite APIs into your applications. - **Architects**: Understand how to model identity and authorization for your use cases. - **DevOps engineers**: See how to automate IndyKite configuration with Terraform. - **Security teams**: Learn how KBAC provides fine-grained, knowledge-based access control. # How should I use these demos? - **Watch in order**: Start with environment setup before moving to advanced topics. - **Follow along**: Open the IndyKite Hub and replicate the steps shown. - **Reference the guides**: Use the written guides for detailed syntax and configuration options. - **Try the examples**: Apply what you learn using the Developer Hub Resources. # Video Demos # Additional Resources - **Getting Started**: Sandbox Guide - **Credentials**: Credentials Guide - **Authorization**: AuthZEN Guide - **ContX IQ**: CIQ Guide - **Terraform**: Terraform Guide - **Code Examples**: Developer Hub Resources - **Community**: IndyKite Forum --- Source: https://developer.indykite.com/guides/guide-demos --- # How Knowledge Graphs power dynamic authorization? > What Knowledge Graphs, KBAC and IndyKite are bringing to dynamic authorization **Category:** KBAC ## Summary Dynamic authorization is transformed with the latest authorization structures like KBAC. ## Content # What is Dynamic Authorization? Dynamic authorization is an adaptive, real-time decision-making system that evaluates access requests based on current context rather than static, pre-defined rules. The modern digital landscape demands **fine-grained, context-aware authorization** because: - Systems are increasingly interconnected. - IoT devices and AI agents require machine-to-machine authorization. - Business rules and organizational structures change constantly. - Users expect personalized, frictionless experiences. # Why do traditional access control models fall short? Traditional models struggle with the complexity of modern systems: Model How it works Limitation ACL Simple lists of who can access what Policy sprawl as systems grow RBAC Access based on assigned roles Role explosion in complex organizations ABAC Access based on attribute matching Complex attribute management and policy rules ReBAC Access based on relationship traversal Requires data to be pre-modeled as relationships # What are Knowledge Graphs? A Knowledge Graph (KG) is a data structure that models entities (nodes) and their relationships (edges), with semantic meaning defined by ontologies. Think of it as a living, constantly updated database that connects: - **Who the user is** (their status, roles, attributes) - **What resources exist** (their state, sensitivity, ownership) - **How entities relate** (organizational hierarchy, group membership, data ownership) - **Contextual factors** (time, location, device, environmental conditions) ### How do Knowledge Graphs enable better authorization? - **Semantic modeling**: Represent complex interconnections with rich meaning, beyond simple attributes. - **Advanced reasoning**: Deduce implicit information and derive new insights for intelligent decisions. - **Unified data**: Integrate information from disparate sources into a single, cohesive view. - **Real-time context**: Enable rapid decisions based on current state, not cached data. - **Intuitive policies**: Represent policies as graph paths, reducing complexity. - **Auditability**: Provide clear, traceable paths for every decision. - **Scalability**: Graph databases are optimized for traversing complex relationships at scale. # What is KBAC? **Knowledge-Based Access Control (KBAC)** is an authorization model that leverages Knowledge Graphs and AI to make intelligent, context-aware access decisions. KBAC builds upon ReBAC (Relationship-Based Access Control) and augments ABAC by adding a semantic layer for advanced reasoning. ### What can KBAC do that other models cannot? - **Real-time decisions**: Evaluate access based on current context, not pre-defined rules. - **Predictive authorization**: Anticipate future access needs based on patterns. - **Intent-aware decisions**: Assess what a user is trying to accomplish, not just what they're requesting. - **AI guardrails**: Provide deterministic, auditable rules for AI agents to operate within secure boundaries. - **Hyper-granular control**: Make access decisions down to specific data elements. ### How does KBAC compare to other models? Aspect Traditional Models KBAC Granularity Coarse to fine Hyper-fine, contextual Context-awareness Low to medium Real-time, predictive, intent-aware Scalability Poor to good Excellent (AI-augmented) Management complexity Low to high Low with visual tools Decision factor Lists, roles, attributes Semantic inference and reasoning # What is AuthZEN? **AuthZEN** is an OpenID Foundation initiative that standardizes communication protocols for externalized, fine-grained authorization. ### Why does AuthZEN matter? - **Interoperability**: Standard protocol for authorization requests across different systems. - **Simplified integration**: Transforms complex N*M integrations into manageable N+M scenarios. - **Vendor independence**: No lock-in to proprietary authorization solutions. Knowledge Graphs and KBAC align with AuthZEN by providing the rich, interconnected data and sophisticated policy evaluation needed for standardized, fine-grained decisions. Specification: https://openid.net/wg/authzen/ # How does IndyKite implement KBAC? IndyKite's platform is built on the **Identity Knowledge Graph (IKG)**, a real-world network of human and non-human entities and their relationships. ### What makes IndyKite's approach unique? Capability How it works Benefit **Identity Knowledge Graph** Unified graph for all human and non-human entities Holistic view of identities and relationships **KBAC Policy Engine** Policies built directly on the IKG Smarter, adaptive access control **Real-time Context** Responsive to live data and policy changes Decisions based on current state **Visual Policy Design** Low-code/no-code drag-and-drop tools Faster deployment, lower technical barrier **Open Standards** AuthZEN, OAuth/OpenID, MCP compliance No vendor lock-in, seamless integration **Scalability** Designed for billions of entities Future-proof for IoT and AI agents # How does real-time context improve authorization? ### What problem does it solve? Static authorization rules cannot adapt to changing circumstances. A user's access needs may vary based on time, location, device, or current task. ### How does the Knowledge Graph help? The IKG maintains current state for all entities, enabling the authorization system to consider: - User's current status and permissions - Resource's current state and availability - Environmental factors (time, location, device) - Recent activity patterns and anomalies This enables "just-in-time" decisions based on the current situation rather than pre-defined rules. # How does low-code policy design work? ### What problem does it solve? Traditional authorization systems require code changes to modify policies, making updates slow and error-prone. ### How does IndyKite address this? IndyKite provides visual tools for designing authorization policies: - Drag-and-drop policy builder - No coding required for policy creation - Simulation tools to test policy impact before deployment - Visual analysis to identify over-privileges or security gaps The structured nature of the KG enables "what-if" analysis to understand policy changes before they take effect. # How does KBAC enable hyper-personalization? ### What problem does it solve? Generic access rules cannot deliver personalized experiences. Users expect tailored interactions based on their context and history. ### How does KBAC help? The deep contextual understanding from the IKG enables: - Tailored access based on user context and preferences - Proactive risk identification based on behavioral patterns - Detection of policy conflicts or security vulnerabilities - Streamlined, consistent access experiences # How does KBAC unify siloed data? ### What problem does it solve? Authorization information is often scattered across multiple systems: user directories, resource catalogs, policy stores, and application databases. ### How does the Knowledge Graph help? The IKG serves as a **central hub** that: - Integrates data from multiple sources into a single graph - Connects entities with meaningful relationships - Becomes the single source of truth for authorization policies - Enables distributed enforcement while maintaining centralized policy management Each enforcement point (microservices, APIs, applications) queries the central IKG for authorization decisions, ensuring consistency across the enterprise. # How does KBAC create business value? ### What problem does it solve? Identity management is typically viewed as a cost center focused on compliance and security. ### How does KBAC transform this? KBAC transforms identity from a liability into an asset by enabling: - **Hyper-personalization**: Deliver tailored experiences based on deep identity understanding. - **Secure data sharing**: Enable granular, controlled access to sensitive data. - **New revenue opportunities**: Monetize identity insights through targeted recommendations. - **Improved loyalty**: Frictionless, consistent user experiences increase retention. - **Proactive security**: Real-time risk identification prevents breaches. # How does KBAC scale for future identities? ### What challenges are coming? The number of digital identities is exploding with IoT devices, AI agents, and machine-to-machine interactions. Traditional systems cannot handle this scale. ### How does IndyKite address this? - **Graph database optimization**: Efficient traversal of complex relationships at scale. - **Flexible data model**: Designed for billions of human and non-human entities. - **Real-time performance**: High-volume authorization requests with low latency. - **AI-ready architecture**: Built to provide guardrails for AI agents and autonomous systems. # Summary Knowledge Graphs are essential for modern dynamic authorization because they: - Provide real-time, contextual understanding of entities and relationships. - Enable KBAC to make intelligent, predictive authorization decisions. - Support standardization through AuthZEN compliance. - Transform identity from a cost center into a business asset. IndyKite's implementation demonstrates how this combination delivers secure, scalable, and intelligent access control that drives business value. ### Next steps - Learn about AuthZEN: AuthZEN Guide - Trust Score in authorization: Use data quality in authorization decisions - Create KBAC policies: Developer Hub Resources - Explore CIQ queries: CIQ Guide --- Source: https://developer.indykite.com/guides/guide-dynamic-authz --- # Creating an IndyKite Environment > Understanding the platform hierarchy and what components are needed to integrate with IndyKite. **Category:** Environment ## Summary Complete guide to setting up your IndyKite environment with the right components. ## Content # Why do I need an IndyKite environment? IndyKite is a real-time data retrieval and enforcement platform that enables context-aware authorization and identity management. Before you can use any IndyKite capability, you need to set up an environment that: - **Stores your data**: The Identity Knowledge Graph (IKG) holds nodes and relationships representing your entities. - **Authenticates your application**: Credentials identify your application and authorize API calls. - **Enforces policies**: Authorization rules are evaluated against data in your IKG. - **Isolates your project**: Each project has its own IKG, ensuring data separation. # What is the IndyKite platform hierarchy? IndyKite uses a hierarchical structure to organize resources. Understanding this hierarchy is essential for setting up your environment correctly. **Level** **Component** **Purpose** **What it contains** 1 **Organization** Top-level account container Service Accounts, Projects 2 **Service Account** Config API authentication Credentials for managing configurations 3 **Project** Isolated working environment Own IKG, Applications, Policies, Knowledge Queries 4 **Application** Represents your software system Application Agents 5 **Application Agent** API authentication identity Credentials (tokens) for API calls # What is an Organization? An Organization is your top-level account in IndyKite. It is created when you sign up for the platform. ### What does an Organization contain? - **Service Accounts**: Credentials for managing configurations via the Config API. - **Projects**: Isolated environments with their own Identity Knowledge Graphs. - **Organization ID**: Unique identifier used when creating resources programmatically. ### Why does the Organization matter? The Organization establishes your account boundary and billing scope. All resources you create belong to your Organization, and Service Account credentials are created at this level. # What is a Service Account? A Service Account provides credentials for the **Config API**, which manages IndyKite configurations. ### What can I do with Service Account credentials? - Create, read, update, and delete Projects - Create Applications and Application Agents - Create and manage KBAC policies - Create and manage Knowledge Queries (CIQ) - Configure External Data Resolvers - Configure Trust Score Profiles - Configure Token Introspection - Configure Outbound Events - Use Terraform to manage configurations as code ### How do I create Service Account credentials? - Log in to the IndyKite Hub at your Organization level. - Navigate to Service Accounts. - Create a new Service Account. - Generate credentials for the Service Account. - Download the credentials JSON file. Hub: https://eu.hub.indykite.com/service-accounts ### How do I use Service Account credentials? **For REST API calls:** Extract the `token` field from your credentials file and use it in the Authorization header: `Authorization: Bearer ` **For Terraform:** Set the environment variable: `export INDYKITE_SERVICE_ACCOUNT_CREDENTIALS_FILE=/path/to/credentials.json` # What is a Project? A Project (also called Application Space) is an isolated working environment with its own Identity Knowledge Graph. ### What does a Project contain? - **Identity Knowledge Graph (IKG)**: A Neo4j graph database storing your nodes and relationships. - **Applications**: Software systems that interact with the IKG. - **Authorization Policies**: KBAC rules that govern access to data. - **Knowledge Queries**: CIQ queries that read or modify graph data. - **External Data Resolvers**: Configurations for fetching data from external APIs during query execution. - **Trust Score Profiles**: Configurations for assessing data quality based on freshness, origin, and verification. - **Token Introspect configurations**: Rules for validating external access tokens. - **Outbound Event configurations**: Settings for streaming events to external systems. ### Why is the IKG important? The Identity Knowledge Graph is the foundation of IndyKite. All authorization decisions, data queries, and contextual operations are performed against data in your IKG. Before using any IndyKite product, you must capture data into your IKG. ### What IKG options do I have? **Option** **Description** **Best for** **Managed IKG** IndyKite hosts and manages the graph database Quick start, no database management **Bring Your Own DB** Connect your own Neo4j database Existing Neo4j investment, custom requirements For managed IKG, specify the `ikg_size` and `region` when creating the project. For your own database, provide the `db_connection` with URL, username, password, and database name. ### How do I create a Project? **Option 1: Hub UI** - Navigate to your Organization in the Hub. - Click "Create Project". - Enter project name and IKG configuration. - Save the project. **Option 2: REST API** Use the Config API with Service Account credentials: Endpoint: https://openapi.indykite.com/api-documentation-config **Option 3: Terraform** Use the `indykite_application_space` resource: Example: Environment Configuration Terraform # What is an Application? An Application represents your software system within a Project. It serves as a container for Application Agents. ### Why do I need an Application? - **Logical grouping**: Organize multiple agents (e.g., backend, mobile, web) under one application. - **_Application node**: When created, an `_Application` node is automatically added to your IKG. - **Service identity**: The Application can act as a subject in CIQ queries without a user access token. ### How do I create an Application? Create an Application within your Project using the Hub UI, REST API, or Terraform. The `indykite_application` resource requires an `app_space_id` (Project ID). # What is an Application Agent? An Application Agent is the identity that authenticates your API calls. Each agent has credentials (tokens) used to access IndyKite APIs. ### What APIs can I call with Application Agent credentials? **API** **Purpose** **Capture API** Store nodes and relationships in the IKG **CIQ Execute API** Run Knowledge Queries to read/update graph data **Authorization API (AuthZEN)** Evaluate KBAC policies for access decisions **Token Introspect API** Validate access tokens and retrieve identity data **EntityMatching API** Resolve and match identities ### How do I create Application Agent credentials? - Create an Application Agent under your Application. - Generate credentials for the Application Agent. - Download or copy the credential token. ### How do I use Application Agent credentials? Include the token in your HTTP request header: `X-IK-ClientKey: ` # What is the complete environment creation workflow? To create a working IndyKite environment from scratch: **Step** **Action** **Result** 1 Sign up for IndyKite Sandbox Organization created 2 Create Service Account credentials Config API access enabled 3 Create a Project with IKG Isolated environment with graph database 4 Create an Application _Application node added to IKG 5 Create Application Agent + credentials API access enabled for your application 6 Capture data into the IKG Nodes and relationships stored 7 Create policies and queries Authorization and data access configured # What credentials do I need for what? Different IndyKite APIs require different credentials: **Operation** **Credential Type** **Header** Create/manage configurations (Config API) Service Account `Authorization: Bearer ` Capture data AppAgent `X-IK-ClientKey: ` Execute CIQ queries AppAgent `X-IK-ClientKey: ` AuthZEN evaluation AppAgent `X-IK-ClientKey: ` User-context operations AppAgent + User Access Token `X-IK-ClientKey` + `Authorization: Bearer` Terraform Service Account Environment variable # Which API regions are available? IndyKite provides API endpoints in two regions: - **EU Region**: `https://eu.api.indykite.com` - **US Region**: `https://us.api.indykite.com` Choose the region that matches your data residency requirements and Hub instance. To learn how to control where your IKG data physically lives like choosing a project region, adding a read replica, routing nodes to specific locations with a composite database, and location-aware authorization: see the Data Residency & Composite Databases Guide. # How can I automate environment creation? ### Option 1: Developer Hub Quick Start Script The fastest way to set up a complete environment: https://github.com/indykite/developer-hub/tree/master/get-started The script creates: - Project, Application, and Application Agent with credentials - Sample data captured in the IKG - KBAC and CIQ policies - Verification tests ### Option 2: Terraform Manage your environment as infrastructure-as-code: - Terraform Guide: https://developer.indykite.com/guides/guide-terraform - Environment Configuration: terraform-2 - Terraform Provider: IndyKite Terraform Provider ### Option 3: REST API Programmatically create resources using the Config API: Config API Reference # What should I do after creating an environment? Once your environment is ready: - **Capture data**: Use the Capture API to store nodes and relationships in your IKG. - **Create authorization policies**: Define KBAC rules for access control. - **Create Knowledge Queries**: Define CIQ queries for data retrieval and updates. - **Test your integration**: Use the AuthZEN API or CIQ Execute API to verify your setup. # Next Steps - **Quick Start**: Sandbox Guide - **Credentials**: Credentials Guide - **Terraform**: Terraform Guide - **Authorization**: Dynamic Authorization Guide - **CIQ**: ContX IQ Guide - **External Data Resolver**: Fetch data from external APIs - **Trust Score**: Assess data quality - **AuthZEN**: AuthZEN Guide - **Examples**: Developer Hub Resources - **API Reference**: OpenAPI Documentation - **Documentation**: IndyKite Docs --- Source: https://developer.indykite.com/guides/guide-environment --- # External Data Resolver: Fetch External API Data During Queries > How to configure External Data Resolvers and Data References to fetch data from external APIs during ContX IQ query execution. **Category:** External Data ## Summary How to create External Data Resolvers, use Data References (external_value), and combine IKG data with real-time external lookups. ## Content # What is an External Data Resolver? An **External Data Resolver** (EDR) is a configuration that fetches data from external systems (APIs, databases) during ContX IQ query execution. A **Data Reference** is the property on a node that points to the resolver using `external_value`. Together, they enable **real-time data lookups** without storing sensitive data in the IKG. Key benefits: - **Keep sensitive data external**: Store VINs, SSNs, or other sensitive data in your secure systems. - **Real-time lookups**: Always get current data, not stale copies. - **Single source of truth**: Avoid data duplication and sync issues. - **Hybrid queries**: Combine IKG graph data with external API responses. # How does it work? The flow involves three components working together: - **External Data Resolver**: Configuration defining how to call an external API (URL, method, headers, response mapping). - **Data Reference**: A node property using `external_value` that references the resolver instead of storing a value directly. - **ContX IQ query**: When executed, automatically resolves data references by calling the configured resolver. **External Data Resolver & Data Reference Flow** ## Example scenario A car rental app needs vehicle VIN numbers, but VINs are stored in a separate vehicle registry: - **IKG stores**: Person(Alice) -[ACCEPTED]-> Contract -[COVERS]-> Vehicle(car2) with category="Car" - **External registry stores**: car2.vin = "1HGBH41JXMN109186" Query execution: - User (Alice) requests vehicle details including VIN - ContX IQ policy authorizes: Alice can READ vehicles covered by her contracts - Query fetches category from IKG: "Car" - Query detects data reference (`external_value`) on vin property - Resolver calls external API to fetch VIN - Response: `{category: "Car", vin: "1HGBH41JXMN109186"}` # What credentials do I need? - **Creating resolvers**: Service Account credentials (Config API) - **Ingesting nodes with data references**: AppAgent credentials (Capture API) - **Executing queries**: AppAgent credentials + optional user access token Configuration methods: - Terraform: indykite_external_data_resolver resource - REST API: Config API documentation # External Data Resolver Configuration ## REST API Endpoints **Operation** **Method** **Endpoint** Create POST `/configs/v1/external-data-resolvers` Read by ID GET `/configs/v1/external-data-resolvers/{id}` Read by name GET `/configs/v1/external-data-resolvers/{name}?location={project_id}` List all GET `/configs/v1/external-data-resolvers?project_id={id}` Update PUT `/configs/v1/external-data-resolvers/{id}` Delete DELETE `/configs/v1/external-data-resolvers/{id}` ## Create Request Syntax { "project_id": "", "name": "", "display_name": "", "description": "", "url": "", "method": "", "headers": { "": ["", ""] }, "request_content_type": "", "request_payload": "", "response_content_type": "", "response_selector": "" } ### What does each field mean? Required fields - `project_id`: The GID of the project where the resolver will be created. - `name`: Unique, immutable identifier for the resolver. Used in data references (`external_value`). - `url`: The full endpoint URL to invoke. Supports parameter substitution (see below). - `method`: HTTP method. Supported values: `GET`, `POST`, `PUT`, `PATCH`. - `request_content_type`: Content type for requests. Currently only `JSON` is supported. - `response_content_type`: Content type for responses. Currently only `JSON` is supported. - `response_selector`: JSON path to extract data from the response (e.g., `.data`, `.result.value`). Optional fields - `display_name`: Human-readable name (can be updated). - `description`: Description of the resolver (max 65000 UTF-8 bytes). - `headers`: HTTP headers to include in requests. Each header can have multiple values. - `request_payload`: JSON body for POST/PUT/PATCH requests (as a string). ### URL Parameter Substitution The URL can include dynamic parameters using the `{$...}` syntax: { "url": "https://api.example.com/vehicles/{$vehicle.external_id}/vin?format={$format || json}" } - `{$vehicle.external_id}`: Substituted with the node's external_id at runtime. - `{$format || json}`: Uses "json" as default if format parameter is not provided. ### Response Selector The `response_selector` uses jq-like JSON path syntax to extract values: **Selector** **API Response** **Extracted Value** `.vin` `{"vin": "ABC123"}` `"ABC123"` `.data.value` `{"data": {"value": "XYZ"}}` `"XYZ"` `.results[0]` `{"results": ["first", "second"]}` `"first"` `.echo` `{"echo": {"nested": "data"}}` `{"nested": "data"}` ## Example: Create External Data Resolver { "project_id": "gid:AAAABbbbCCCC...", "name": "vin-lookup-resolver", "display_name": "Vehicle VIN Lookup", "description": "Fetches vehicle VIN from external registry", "url": "https://vehicle-registry.example.com/api/v1/vehicles/{$vehicle_id}", "method": "GET", "headers": { "Authorization": ["Bearer api-key-12345"], "X-API-Version": ["2024-01"] }, "request_content_type": "JSON", "response_content_type": "JSON", "response_selector": ".data.vin" } ## Example: POST Request with Payload { "project_id": "gid:AAAABbbbCCCC...", "name": "enrichment-service", "display_name": "Data Enrichment Service", "description": "Enriches node data via POST request", "url": "https://enrichment.example.com/api/enrich", "method": "POST", "headers": { "Authorization": ["Bearer secret-token"] }, "request_content_type": "JSON", "request_payload": "{\"lookup_type\": \"vehicle\", \"fields\": [\"vin\", \"registration\"]}", "response_content_type": "JSON", "response_selector": ".enriched_data" } # Using external_value (Data References) in Nodes ## What is external_value? A **data reference** is created when you use `external_value` instead of `value` for a property. The `external_value` points to an External Data Resolver, which fetches the actual value from an external system when queried via ContX IQ. ### How can I reference a resolver? The `external_value` accepts multiple reference formats: **Format** **Example** **Description** By name `"external_value": "vin-lookup-resolver"` Reference resolver by its unique name By GID `"external_value": "gid:AAAABbbbCCCC..."` Reference resolver by its configuration ID Parameter `"external_value": "$resolver_ref"` Dynamic reference via input parameter (in Knowledge Query upserts) ## Node with external_value Syntax **Using resolver name (string):** { "external_id": "car2", "type": "Vehicle", "properties": [ { "type": "category", "value": "Car" }, { "type": "vin", "external_value": "vin-lookup-resolver" } ] } **Using resolver GID:** { "external_id": "car2", "type": "Vehicle", "properties": [ { "type": "category", "value": "Car" }, { "type": "vin", "external_value": "gid:AAAABWtpcmtlby1jb25maWcAACRleHRlcm5hbC1kYXRhLXJlc29sdmVyL..." } ] } In these examples: - `category` has a regular `value` stored in the IKG. - `vin` uses `external_value` pointing to the resolver (by name or GID). ## Capture API Request POST /capture/v1/nodes { "nodes": [ { "external_id": "alice", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "alice@email.com" }, { "type": "given_name", "value": "Alice" } ] }, { "external_id": "car2", "type": "Vehicle", "properties": [ { "type": "category", "value": "Car" }, { "type": "is_active", "value": true }, { "type": "vin", "external_value": "vin-lookup-resolver" } ] } ] } # ContX IQ Integration ## Policy for External Data Access The CIQ policy defines the graph pattern and what can be read. External data properties are accessed through the same `allowed_reads` as regular properties. { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "Person" }, "condition": { "cypher": "MATCH (company:Company)-[:OFFERS]->(contract:Contract)(vehicle:Vehicle)", "filter": [ { "attribute": "subject.external_id", "operator": "=", "value": "$subject_external_id" } ] }, "allowed_reads": { "nodes": [ "vehicle", "vehicle.*" ] } } The `vehicle.*` wildcard allows reading all vehicle properties, including data references (properties with `external_value`). ## Knowledge Query Requesting External Data The knowledge query specifies which properties to return. External value properties are requested the same way as regular properties: { "nodes": [ "vehicle", "vehicle.property.category", "vehicle.property.vin" ] } When executed: - `vehicle.property.category`: Returns value from IKG ("Car") - `vehicle.property.vin`: Triggers resolver call, returns external value ("vinmagic") ## Execution and Response Execute the query via ContX IQ: POST /contx-iq/v1/execute { "id": "knowledge_query_gid", "input_params": { "subject_external_id": "alice" } } Response with combined IKG + external data: { "data": [ { "nodes": { "vehicle": { "Labels": ["Resource", "Vehicle"], "Props": { "external_id": "car2", "type": "Vehicle" } }, "vehicle.property.category": "Car", "vehicle.property.vin": "vinmagic" } } ] } # Terraform Configuration Use the `indykite_external_data_resolver` resource: resource "indykite_external_data_resolver" "vin_lookup" { location = indykite_application_space.my_app_space.id name = "vin-lookup-resolver" display_name = "Vehicle VIN Lookup" description = "Fetches vehicle VIN from external registry" url = "https://vehicle-registry.example.com/api/v1/vehicles" method = "GET" request_type = "json" response_type = "json" response_selector = ".data.vin" headers { name = "Authorization" values = ["Bearer ${var.api_key}"] } headers { name = "X-API-Version" values = ["2024-01"] } } ### Terraform Arguments Reference **Argument** **Required** **Description** `location` Yes Application Space ID where the resolver is created `name` Yes Unique, immutable identifier `url` Yes External API endpoint URL `method` Yes HTTP method (GET, POST, PUT, PATCH) `request_type` Yes Request content type (json) `response_type` Yes Response content type (json) `response_selector` Yes JSON path to extract value `display_name` No Human-readable name `description` No Resource description `headers` No Request headers block `request_payload` No JSON body for POST/PUT/PATCH # Error Handling **HTTP Code** **Meaning** **Common Cause** 400 Bad Request Invalid JSON, missing required fields 401 Unauthorized Invalid or missing Bearer token 403 Forbidden Insufficient permissions for the project 404 Not Found Resolver ID/name doesn't exist 412 Precondition Failed ETag mismatch (concurrent modification) 422 Unprocessable Entity Validation error (invalid URL, method, etc.) # Best Practices ### Security - Store API keys and tokens securely; use environment variables in Terraform. - Use HTTPS endpoints only for external APIs. - Implement proper authentication on your external endpoints. ### Performance - External resolver calls add latency to queries. Use sparingly for truly external data. - Consider caching on your external API if data doesn't change frequently. - Keep response payloads small; use `response_selector` to extract only needed data. ### Design - Use descriptive resolver names that indicate the data source. - One resolver per data type/source for better maintainability. - Document the expected response format for each resolver. # Complete Example See the full working example at: ContX IQ: Query External Data Sources via Data Resolver # Next Steps - ContX IQ guide: ContX IQ Guide - Terraform provider: External Data Resolver Resource - REST API reference: Config API - Credentials guide: Credentials Guide --- Source: https://developer.indykite.com/guides/guide-external-data-resolver --- # Why use a Graph Database? > Understanding why graph databases are essential for identity, authorization, and the IndyKite platform. **Category:** Environment ## Summary Why graph databases outperform relational databases for connected data and authorization use cases. ## Content # What is a Graph Database? A graph database stores data as **nodes**, **relationships**, and **properties** instead of tables and rows. This structure mirrors how data exists in the real world—as interconnected entities with meaningful relationships. Think of it as sketching ideas on a whiteboard: you draw circles (entities) and connect them with arrows (relationships). A graph database stores data exactly that way. ### What are the core components? **Component** **Description** **Example** **Node** An entity or object in your domain `Person`, `Document`, `Application` **Label** A tag that classifies nodes into groups `:User`, `:Admin`, `:Resource` **Relationship** A named, directed connection between two nodes `OWNS`, `MEMBER_OF`, `CAN_ACCESS` **Property** A key-value pair on nodes or relationships `email: "alice@example.com"` # Why are relationships important? In graph databases, **relationships are first-class citizens**—they are stored natively alongside nodes, not computed at query time. This is fundamentally different from relational databases, where relationships are represented as foreign keys and computed through JOIN operations. ### What does "first-class citizen" mean? - **Stored natively**: Each node physically points to its connected nodes. - **Always available**: Relationships exist as persistent data, not derived from keys. - **Rich with properties**: Relationships can have their own attributes (e.g., `GRANTED_ON: "2024-01-15"`). - **Directional**: Every relationship has a start node, end node, and type. # How do graph databases compare to relational databases? ### Why do relational databases struggle with connected data? Relational databases were designed for structured, tabular data. When data becomes highly connected, they face significant challenges: **Challenge** **Relational Database** **Graph Database** **Representing relationships** Foreign keys in separate tables Native relationship objects **Querying relationships** JOIN operations across tables Direct traversal between nodes **Multi-hop queries** Multiple JOINs, exponential complexity Simple path traversal **Schema changes** ALTER TABLE, migrations, downtime Add nodes/relationships on the fly **Performance at scale** Degrades with more JOINs Consistent regardless of data size ### What is the JOIN problem? In relational databases, answering the question "Can Alice access Document X?" might require: - Query the `users` table to find Alice. - JOIN with `user_groups` to find her groups. - JOIN with `group_roles` to find roles for those groups. - JOIN with `role_permissions` to find permissions for those roles. - JOIN with `resource_permissions` to check if any permission grants access to Document X. Each JOIN adds latency and computational cost. As the number of hops increases, performance degrades exponentially. ### How does a graph database solve this? The same query in a graph database: MATCH (alice:User {email: "alice@example.com"})-[:MEMBER_OF]->(:Group)-[:HAS_ROLE]->(:Role)-[:CAN_ACCESS]->(doc:Document {id: "X"}) RETURN doc The graph database traverses from Alice through her groups, roles, and permissions in a single operation. Performance remains consistent regardless of data size because it only visits the nodes connected to Alice—not the entire dataset. # When should I use a graph database? Graph databases excel when **relationships between data points matter more than the individual points themselves**. ### What are the ideal use cases? **Use Case** **Why Graphs Work Better** **Identity & Access Management** Users, groups, roles, permissions form a natural hierarchy of relationships **Authorization** Access decisions depend on traversing relationship paths **Fraud Detection** Detecting fraud requires finding suspicious patterns across connected entities **Recommendation Engines** "Users who liked X also liked Y" is a relationship query **Knowledge Graphs** Representing semantic relationships between concepts **Network & IT Operations** Infrastructure components are interconnected by nature **Master Data Management** Connecting data across siloed systems ### When are relational databases still appropriate? - Primarily tabular data with few relationships. - Simple CRUD operations on isolated records. - Transactional systems with rigid schemas. - Reporting and analytics on flat data. # Why is a graph database essential for authorization? Authorization is fundamentally about relationships: - Does this **user** have a **relationship** to this **resource** that permits this **action**? - Is this **user** a **member of** a **group** that has a **role** with **permission** to access this **resource**? These questions are naturally expressed as graph traversals. ### What problems do traditional authorization systems have? **Problem** **With Relational DB** **With Graph DB** **Role explosion** Create thousands of roles to cover all combinations Model relationships directly, no artificial roles needed **Policy sprawl** Maintain long ACLs for every resource Define policies as graph patterns **Complex hierarchies** Recursive queries or denormalization Natural hierarchy traversal **Real-time decisions** Cached permissions, stale data Live traversal of current state **Audit trails** Difficult to trace decision path Clear, traceable graph paths # What is the Identity Knowledge Graph (IKG)? The Identity Knowledge Graph is IndyKite's graph database that stores all identity and resource data. It is the foundation of the IndyKite platform. ### What does the IKG contain? - **Human identities**: Users, customers, employees with their attributes. - **Non-human identities**: Applications, services, devices, AI agents. - **Resources**: Documents, APIs, data, any protected asset. - **Relationships**: How all these entities connect to each other. - **Context**: Attributes, metadata, and environmental data. ### Why is the IKG important for IndyKite? Every IndyKite capability operates on the IKG: **Capability** **How it uses the IKG** **Capture** Stores nodes and relationships in the graph **KBAC (AuthZEN)** Evaluates policies by traversing graph relationships **ContX IQ (CIQ)** Reads and updates graph data with authorization **Token Introspect** Maps token claims to nodes in the graph **Outbound Events** Triggers events when graph data changes # Why is a graph database important for KBAC? **Knowledge-Based Access Control (KBAC)** is IndyKite's authorization model that uses the IKG to make intelligent, context-aware access decisions. ### How does KBAC use the graph? KBAC policies are defined as **graph patterns**. When an authorization request arrives, IndyKite: - Maps the request to nodes in the IKG (subject, resource, action). - Evaluates the policy by traversing relationships in the graph. - Returns a decision based on whether the pattern exists. ### Example: Can Alice drive Car X? Policy definition: { "subject": { "type": "Person" }, "actions": ["CAN_DRIVE"], "resource": { "type": "Car" }, "condition": { "cypher": "MATCH (subject:Person)-[:OWNS]->(resource:Car)" } } This policy says: A `Person` can `CAN_DRIVE` a `Car` if there is an `OWNS` relationship between them. The graph database traverses from Alice to Car X, looking for an `OWNS` relationship. If it exists, access is granted. If not, access is denied. ### Why can't relational databases do this efficiently? - **Variable-depth traversals**: "Can Alice access any document in any project she's a member of?" requires recursive queries. - **Multiple relationship types**: Different paths may grant access (direct ownership, group membership, role assignment). - **Real-time evaluation**: Authorization must be checked at request time, not from cached data. - **Contextual attributes**: Decisions may depend on attributes along the path, not just endpoints. # Why is a graph database important for CIQ? **ContX IQ (CIQ)** delivers authorized data retrieval and mutation. It uses the graph to: - **Define what data can be accessed**: Policies specify graph patterns. - **Query related data**: Knowledge Queries traverse relationships to find results. - **Update connected data**: Create or modify nodes and relationships in context. ### Example: Get license plates for a person's vehicles Policy pattern: `MATCH (person:Person)-[:ACCEPTED]->(contract:Contract)-[:COVERS]->(vehicle:Vehicle)-[:HAS]->(ln:LicenseNumber)` This query traverses from a Person through their Contracts to covered Vehicles and their License Numbers. The graph database follows these relationships efficiently, regardless of how many contracts, vehicles, or license numbers exist in the system. # How does graph performance scale? ### Why is graph traversal fast? Graph databases use **index-free adjacency**: each node directly references its connected nodes. This means: - **No index lookups**: Relationships are stored as direct pointers. - **Local traversal**: Queries only visit relevant nodes, not the entire dataset. - **Constant time per hop**: Adding more data doesn't slow down individual traversals. ### How does this compare to JOIN performance? **Aspect** **Relational JOIN** **Graph Traversal** **Time complexity** O(n × m) for each JOIN O(k) where k = nodes visited **Multi-hop queries** Exponential slowdown Linear with path length **Index dependency** Requires careful index design Built-in via adjacency **Dataset growth** JOINs slow as tables grow Unaffected by total size Graph databases can traverse millions of relationships per second, maintaining consistent performance year over year. # How does schema flexibility help? ### What is schema-optional? Graph databases like Neo4j are **schema-optional**: you can add new node types, relationship types, and properties without schema migrations. ### Why does this matter for identity and authorization? - **Evolving requirements**: Add new entity types (AI agents, IoT devices) without restructuring. - **Heterogeneous data**: Different nodes can have different properties. - **Integration**: Connect data from multiple sources with different schemas. - **Rapid iteration**: Model changes don't require downtime or migrations. ### Example: Adding AI agents to your system With a relational database, adding AI agents as a new identity type might require: - Creating new tables (`ai_agents`, `ai_agent_permissions`). - Modifying existing tables to reference the new tables. - Updating all JOIN queries to include the new tables. - Running migrations and potentially causing downtime. With a graph database: - Create nodes with the `:AIAgent` label. - Create relationships to existing resources. - Existing queries continue to work. - New queries can traverse to AI agents immediately. # What is Cypher? **Cypher** is Neo4j's declarative query language for graph databases. It is designed to express graph patterns intuitively. ### How does Cypher work? Cypher uses ASCII art-like syntax to describe graph patterns: - `(node)` — Represents a node - `-[:RELATIONSHIP]->` — Represents a directed relationship - `{property: value}` — Filters by property ### Example queries **Find all documents Alice can access:** MATCH (alice:User {email: "alice@example.com"})-[:CAN_ACCESS]->(doc:Document) RETURN doc **Find access through group membership:** MATCH (alice:User {email: "alice@example.com"})-[:MEMBER_OF]->(group:Group)-[:CAN_ACCESS]->(doc:Document) RETURN doc **Variable-length path (any depth):** MATCH (alice:User {email: "alice@example.com"})-[:MEMBER_OF*1..5]->(group:Group)-[:CAN_ACCESS]->(doc:Document) RETURN doc ### How does IndyKite use Cypher? IndyKite KBAC and CIQ policies use Cypher in the `condition.cypher` field to define graph patterns. When a policy is evaluated, IndyKite executes the Cypher pattern against the IKG to determine access. # How does the IKG support real-time decisions? ### What problem does real-time authorization solve? Traditional authorization systems often use cached permissions: - Permissions are computed periodically and stored. - Changes take time to propagate. - Stale data can grant access that should be revoked. ### How does the IKG enable real-time decisions? The IKG maintains the current state of all entities and relationships: - **Live data**: Authorization queries traverse current relationships. - **Immediate revocation**: Delete a relationship, access is revoked instantly. - **Contextual factors**: Evaluate time, location, device at request time. - **No cache invalidation**: No need to manage permission caches. # What IKG options does IndyKite provide? When creating a Project in IndyKite, you choose how to provision your IKG: **Option** **Description** **Best for** **Managed IKG** IndyKite hosts and manages the Neo4j database Quick start, no database management overhead **Bring Your Own DB** Connect your own Neo4j instance Existing Neo4j investment, custom requirements ### How do I connect my own Neo4j database? Provide the connection details when creating your Project: - **URL**: Neo4j connection string (e.g., `neo4j+s://xxxxx.databases.neo4j.io`) - **Username**: Database user - **Password**: Database password - **Database name**: The specific database to use You can get a free Neo4j instance from: - Neo4j Aura Console - Neo4j AuraDB - Neo4j Desktop # Summary: Why graphs for IndyKite? The graph database is essential to IndyKite because: **Requirement** **Why Graphs Deliver** **Identity relationships** Users, groups, roles, resources are naturally connected **Authorization decisions** Access depends on relationship paths, not table rows **Real-time evaluation** Traverse current state, not cached permissions **Policy flexibility** Express rules as graph patterns (Cypher) **Performance at scale** Millions of traversals per second, consistent latency **Schema evolution** Add new identity types without migrations **Context awareness** Attributes on nodes and relationships inform decisions **Auditability** Clear paths show why access was granted or denied # Next Steps - **Environment setup**: Environment Guide - **Capture data**: Developer Hub Resources - **KBAC policies**: Dynamic Authorization Guide - **CIQ queries**: ContX IQ Guide - **AuthZEN**: AuthZEN Guide - **Terraform**: Terraform Guide - **Neo4j documentation**: Neo4j Getting Started - **Cypher reference**: Cypher Manual --- Source: https://developer.indykite.com/guides/guide-graph-database --- # How to use the MCP server? > Requirements, access, endpoints to use the MCP. **Category:** MCP ## Summary Requirements, access, endpoints to use the MCP. ## Content # What is the IndyKite MCP Server? The IndyKite MCP (Model Context Protocol) server enables AI agents and LLM applications to interact with IndyKite's authorization and data services. It provides a standardized interface for: - **AuthZEN authorization**: Make access control decisions (evaluate, search resources, search actions). - **ContX IQ (CIQ)**: Execute knowledge queries to read and write graph data. - **Resource discovery**: List available knowledge queries with agent-friendly descriptions. The server implements the Model Context Protocol specification, making it compatible with MCP-enabled AI tools and agents. # What is the MCP URL? The MCP server is available in two regions: - **EU**: https://eu.mcp.indykite.com - **US**: https://us.mcp.indykite.com **Full endpoint URL:** `/mcp/v1/` Replace `` with your IndyKite project GID. # What do I need before using the MCP server? ## Prerequisites **IndyKite environment**: Project, Application, Application Agent, and Application Agent credentials. - See: Environment Setup Example **Token Introspect configuration**: Required to validate user access tokens. - See: Token Introspect Guide **MCP Server configuration**: Binds the runtime MCP endpoint to an AppAgent and a Token Introspect, and declares the OAuth scopes the server advertises. The MCP server will not accept requests for a project until this configuration exists. - See: How do I create an MCP server configuration? below. - **Project GID**: Your IndyKite project identifier. **Data and policies**: Captured data, KBAC policies, and/or CIQ policies and Knowledge Queries. - See: MCP Example # How do I create an MCP server configuration? Before the MCP runtime endpoint will accept requests for your project, you must create an MCP Server configuration. This configuration tells IndyKite which AppAgent and Token Introspect the MCP server should use, and which OAuth scopes it advertises. ## Required fields Field Type Description `name`stringURL-friendly identifier, unique within the project. Immutable. `project_id`string (GID)Project that owns this MCP server configuration. `app_agent_id`string (GID)AppAgent the MCP server uses to call IndyKite APIs at runtime. Needs Authorization API and ContX IQ API permissions. `token_introspect_id`string (GID)Token Introspect configuration used to validate inbound Bearer tokens. `enabled`booleanWhether the MCP server accepts requests. `scopes_supported`string[]OAuth scopes advertised in `.well-known/oauth-protected-resource`. Must contain at least one entry. ## Optional fields - `display_name` (string, 2-254 chars): Human-readable name. - `description` (string, 2-65000 chars): Free-text description. ## Example: create an MCP server configuration curl -X POST /configs/v1/mcp-servers -H "Content-Type: application/json" -H "Authorization: Bearer $SERVICE_ACCOUNT_TOKEN" -d '{ "name": "mcp-server-name", "display_name": "MCP Server name", "description": "MCP Server configuration description", "project_id": "gid-of-project", "app_agent_id": "gid-of-app-agent", "token_introspect_id": "gid-of-token-introspect", "enabled": true, "scopes_supported": ["name", "email"] }' **Response (201 Created):** Returns the new configuration's `id` (GID), `create_time`, `created_by`, and `update_time`. Reference: POST /mcp-servers # How do I authenticate with the MCP server? The MCP server authenticates each request with a single **Bearer token** - the end-user's OAuth 2.0 access token. The AppAgent the server uses to call IndyKite APIs at runtime is resolved server-side from your MCP Server configuration (`app_agent_id`). Clients send only the Bearer token. ## Bearer Token (Authorization) This identifies the user (subject) making the request. - **Header**: `Authorization: Bearer ` - **Source**: OAuth 2.0 access token from your identity provider - **Validation**: Token is introspected using your Token Introspect configuration - **Purpose**: Used as the subject in authorization decisions Configure token introspection: POST /token-introspects ## What happens without a Bearer token? If you call the MCP server without a Bearer token, it returns: - `401 Unauthorized` status - `.well-known/oauth-protected-resource` metadata (per RFC9728) **Note:** Contact IndyKite to have your identity providers and scopes added to the `.well-known/oauth-protected-resource` file for your project. # Why does a multi-audience Bearer token return 401? A multi-audience Bearer token - one whose `aud` claim lists more than one audience - works through the MCP server only if **exactly one** Token Introspect configuration in the relevant app space matches it. The MCP Server configuration binds a single `token_introspect_id`, but that binding does **not** protect you from ambiguity created by other Token Introspect configurations that share the same issuer in that app space. ## Two independent audience checks An inbound token passes through two checks that use different selection logic: - **Pre-check (membership)**: the audience bound to the MCP server must be present in the token's `aud` claim. **Introspection (lookup)**: the token's audiences are matched against the Token Introspect configurations in the app space. - **0 matches** → `NotFound` - **2 or more matches** → `FailedPrecondition`: `"multiple matches for issuer-audiences"` ## Why ambiguity produces a 401 Consider a token with `aud = [A, B]`, an MCP server bound to the Token Introspect for audience `A`, and a second (even unlinked) Token Introspect for audience `B` that shares the same issuer in that app space: - **Pre-check**: `A ∈ [A, B]` → passes. - **Introspection**: matching `ANY('{A,B}')` hits both `A` and `B` → 2 rows → `FailedPrecondition`, which the MCP server maps to a generic `401 invalid_token`. ## Configuration facts to keep in mind - A Token Introspect configuration holds **one** audience; "two audiences" means two separate configuration rows. The combination `(app_space_id, issuer, audience)` is unique. - The MCP Server configuration binds exactly **one** `token_introspect_id`. **To avoid this:** for a given issuer in an app space, make sure a multi-audience token resolves to a single Token Introspect configuration - do not keep multiple Token Introspect configs whose audiences overlap with the same token's `aud` claim. # Which protocol revisions does the MCP server support? The MCP server uses JSON-RPC over HTTP POST and supports two request styles, selected by the protocol version your client sends: Style Protocol revisions How it works **Session-based** Before `2026-07-28` (e.g. `2025-11-25`) `initialize` handshake first; the server returns an `Mcp-Session-Id` header that every follow-up request must send back. **Stateless** `2026-07-28` and later No handshake and no session: every request is self-contained, carrying the protocol metadata in the request's `params._meta` plus the standard MCP headers. See How do I use the stateless protocol? Both styles authenticate the same way (Bearer token) and expose the same tools and resources. You can query the supported revisions at runtime with the stateless `server/discover` method (see below). ## How does the MCP session work? (session-based protocol) - **Initialize**: Send an `initialize` request to start a session. - **Receive Session ID**: The server returns an `Mcp-Session-Id` header. - **Include Session ID**: All subsequent requests must include the `Mcp-Session-Id` header. The server is built using the official MCP Go SDK, so you can also use Go SDK clients to interact with it. # What is the MCP process flow? # How do I make MCP requests? (session-based protocol) ## Step 1: Initialize the MCP session Start a new MCP session and receive a session ID. curl -v -i -X POST /mcp/v1/ -H "Content-Type: application/json" -H "Authorization: Bearer $BEARER_TOKEN" -d '{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-11-25", "capabilities": {}, "clientInfo": {"name": "curl", "version": "1.0"} } }' **Response:** Returns `Mcp-Session-Id` header. Save this for subsequent requests. ## What happens without a Bearer token? curl -v -i -X POST /mcp/v1/ -H "Content-Type: application/json" -d '{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-11-25", "capabilities": {}, "clientInfo": {"name": "curl", "version": "1.0"} } }' **Response:** Returns `401 Unauthorized` and `.well-known/oauth-protected-resource` metadata. ## Step 2: Confirm initialization Verify the session is initialized. curl -v -i -X POST /mcp/v1/ -H "Content-Type: application/json" -H "Authorization: Bearer $BEARER_TOKEN" -H "Mcp-Session-Id: $SESSION_ID" -d '{ "jsonrpc": "2.0", "id": 1, "method": "notifications/initialized", "params": { "protocolVersion": "2025-11-25", "capabilities": {}, "clientInfo": {"name": "curl", "version": "1.0"} } }' # How do I use the stateless protocol? (revision 2026-07-28) Starting with protocol revision `2026-07-28`, the MCP server also accepts **stateless** requests: there is no `initialize`/`initialized` handshake and no `Mcp-Session-Id` - each request stands on its own. This suits serverless and multi-instance clients where holding a session between calls is impractical. ## What every stateless request must carry **In the body**, a `params._meta` object with: - `"io.modelcontextprotocol/protocolVersion"`: the protocol revision, e.g. `"2026-07-28"` (required - a request without it is treated as session-based). - `"io.modelcontextprotocol/clientCapabilities"`: the client's capabilities (`{}` if none). - `"io.modelcontextprotocol/clientInfo"`: optional client name and version. **In the headers**, the standard MCP headers for this revision: - `Mcp-Protocol-Version: 2026-07-28` - `Mcp-Method`: must equal the JSON-RPC `method` in the body (mismatch or absence is rejected). - `Mcp-Name`: required for `tools/call` (the tool name), `resources/read` (the resource URI), and `prompts/get` (the prompt name); must match the body. - The usual `Authorization: Bearer`, `Content-Type: application/json`, and `Accept: application/json, text/event-stream` headers - authentication is unchanged, and responses may arrive as an SSE stream with the JSON result in the event data. No session is created: the response carries no `Mcp-Session-Id` header. If a mixed-version client sends a stale `Mcp-Session-Id` alongside a `2026-07-28` `_meta`, the `_meta` wins and the header is ignored. ## server/discover: check what the endpoint supports The stateless protocol adds a `server/discover` method that returns the server's capabilities and the protocol revisions it accepts - use it to decide which style to speak: curl -v -i -X POST /mcp/v1/ -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" -H "Authorization: Bearer $BEARER_TOKEN" -H "Mcp-Protocol-Version: 2026-07-28" -H "Mcp-Method: server/discover" -d '{ "jsonrpc": "2.0", "id": 1, "method": "server/discover", "params": { "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {}, "io.modelcontextprotocol/clientInfo": {"name": "curl", "version": "1.0"} } } }' **Response:** `200` with `result.supportedVersions` (for example `["2026-07-28", "2024-11-05", …]`), `result.capabilities`, and the server's instructions. No `Mcp-Session-Id` header is returned. ## Example: list tools statelessly curl -v -i -X POST /mcp/v1/ -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" -H "Authorization: Bearer $BEARER_TOKEN" -H "Mcp-Protocol-Version: 2026-07-28" -H "Mcp-Method: tools/list" -d '{ "jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": { "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {} } } }' ## Example: call a tool statelessly The same `authzen_evaluate` call as in the session-based section - note the `Mcp-Name` header naming the tool, and the `_meta` object inside `params` next to the tool arguments: # random values to adapt in arguments # subject_id is Bearer token sub curl -v -i -X POST /mcp/v1/ -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" -H "Authorization: Bearer $BEARER_TOKEN" -H "Mcp-Protocol-Version: 2026-07-28" -H "Mcp-Method: tools/call" -H "Mcp-Name: authzen_evaluate" -d '{ "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "authzen_evaluate", "arguments": { "subject_type": "Person", "subject_id": "alice", "resource_type": "Car", "resource_id": "cadillacv16", "action_name": "CAN_DRIVE" }, "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {} } } }' **Response:** `200` with the tool result in `result.content` (a `text` item whose body holds the JSON decision), exactly as in the session-based flow - and no `Mcp-Session-Id` header. ## What happens with an unsupported protocol version? Requesting a protocol revision the server does not support returns `400` with a JSON-RPC error naming what was asked for and what is available: { "error": { "code": -32022, "message": "unsupported protocol version", "data": { "requested": "2099-01-01", "supported": ["2026-07-28", "..."] } } } # How do I discover available resources and tools? The examples below use the session-based style. On the stateless protocol, drop the `Mcp-Session-Id` header and add the `_meta` object and standard headers shown above instead - the methods, tools, and arguments are identical. ## List MCP resources Discover what resources are available in the MCP server. curl -v -i -X POST /mcp/v1/ -H "Content-Type: application/json" -H "Authorization: Bearer $BEARER_TOKEN" -H "Mcp-Session-Id: $SESSION_ID" -d '{ "jsonrpc": "2.0", "id": 2, "method": "resources/list", "params": {} }' ## List MCP tools Discover what tools are available for the AI agent to call. curl -v -i -X POST /mcp/v1/ -H "Content-Type: application/json" -H "Authorization: Bearer $BEARER_TOKEN" -H "Mcp-Session-Id: $SESSION_ID" -d '{ "jsonrpc": "2.0", "id": 3, "method": "tools/list", "params": {} }' ## List Knowledge Queries Get a list of available CIQ Knowledge Queries with agent-friendly descriptions. curl -v -i -X POST /mcp/v1/ -H "Content-Type: application/json" -H "Authorization: Bearer $BEARER_TOKEN" -H "Mcp-Session-Id: $SESSION_ID" -d '{ "jsonrpc": "2.0", "id": 4, "method": "resources/read", "params": { "uri": "indykite://knowledge-queries/" } }' **Response:** Returns list of Knowledge Query IDs and descriptions, formatted for AI agents to understand how to call the `ciq_execute` tool. # What tools are available? ## AuthZEN Tools These tools make authorization decisions based on KBAC policies. Tool Description `authzen_evaluate` Check if a subject can perform an action on a resource `authzen_evaluations` Batch evaluate multiple authorization requests `authzen_search_resource` Find all resources a subject can access with a given action `authzen_search_action` Find all actions a subject can perform on a resource ## CIQ Tools Tool Description `ciq_execute` Execute a Knowledge Query to read or write graph data # How do I use the AuthZEN tools? ## authzen_evaluate: Single authorization check Check if a subject can perform a specific action on a resource. # random values to adapt in arguments # subject_id is Bearer token sub curl -v -i -X POST /mcp/v1/ -H "Content-Type: application/json" -H "Authorization: Bearer $BEARER_TOKEN" -H "Mcp-Session-Id: $SESSION_ID" -d '{ "jsonrpc": "2.0", "id": 5, "method": "tools/call", "params": { "name": "authzen_evaluate", "arguments": { "subject_type": "Person", "subject_id": "alice", "resource_type": "Car", "resource_id": "cadillacv16", "action_name": "CAN_DRIVE" } } }' ## authzen_evaluations: Batch authorization checks Evaluate multiple authorization requests in a single call. # random values to adapt in arguments # subject_id is Bearer token sub curl -v -i -X POST /mcp/v1/ -H "Content-Type: application/json" -H "Authorization: Bearer $BEARER_TOKEN" -H "Mcp-Session-Id: $SESSION_ID" -d '{ "jsonrpc": "2.0", "id": 6, "method": "tools/call", "params": { "name": "authzen_evaluations", "arguments": { "subject_type": "user", "subject_id": "user-123", "evaluations": [ {"action": {"name": "read"}, "resource": {"type": "doc", "id": "doc1"}}, {"action": {"name": "write"}, "resource": {"type": "doc", "id": "doc2"}} ] } } }' ## authzen_search_resource: Find accessible resources Find all resources of a given type that a subject can access with a specific action. # random values to adapt in arguments # subject_id is Bearer token sub curl -v -i -X POST /mcp/v1/ -H "Content-Type: application/json" -H "Authorization: Bearer $BEARER_TOKEN" -H "Mcp-Session-Id: $SESSION_ID" -d '{ "jsonrpc": "2.0", "id": 7, "method": "tools/call", "params": { "name": "authzen_search_resource", "arguments": { "subject_type": "User", "subject_id": "user-123", "action_name": "READ", "resource_type": "Document" } } }' ## authzen_search_action: Find permitted actions Find all actions a subject can perform on a specific resource. # random values to adapt in arguments # subject_id is Bearer token sub curl -v -i -X POST /mcp/v1/ -H "Content-Type: application/json" -H "Authorization: Bearer $BEARER_TOKEN" -H "Mcp-Session-Id: $SESSION_ID" -d '{ "jsonrpc": "2.0", "id": 8, "method": "tools/call", "params": { "name": "authzen_search_action", "arguments": { "subject_type": "User", "subject_id": "user-123", "resource_type": "Document", "resource_id": "doc-456" } } }' # How do I use the CIQ tool? ## ciq_execute: Run a Knowledge Query Execute a CIQ Knowledge Query to read or write data in the Identity Knowledge Graph. # random keys/values to adapt in input_params curl -v -i -X POST /mcp/v1/ -H "Content-Type: application/json" -H "Authorization: Bearer $BEARER_TOKEN" -H "Mcp-Session-Id: $SESSION_ID" -d '{ "jsonrpc": "2.0", "id": 9, "method": "tools/call", "params": { "name": "ciq_execute", "arguments": { "id": ", "input_params": {"license": "AL98745", "app_external_id": "applicationParking"} } } }' ## What arguments does ciq_execute need? Argument Description `id` The GID or name of the Knowledge Query to execute `input_params` Key-value pairs for partial filter variables defined in the query **Tip:** Use the `resources/read` method with URI `indykite://knowledge-queries/` to get agent-friendly descriptions of available queries and their required parameters. # Next Steps - MCP example (session-based protocol): MCP Resource Example - MCP example (stateless protocol 2026-07-28): Stateless MCP Resource Example - Environment setup: Environment Setup - AuthZEN guide: AuthZEN Guide - CIQ guide: ContX IQ Guide - Token Introspect: Token Introspect Guide - MCP specification: Model Context Protocol - Go SDK: MCP Go SDK --- Source: https://developer.indykite.com/guides/guide-mcp --- # Outbound Events: Stream Graph Changes to Kafka, Event Grid, Service Bus, and Pub/Sub > What is needed to configure Events for the IndyKite platform. **Category:** Outbound Events ## Summary What are the key components and steps for creating an event sink configuration to send messages to external providers? ## Content # What are Outbound Events? Outbound Events (Signals) are real-time notifications sent to external systems when changes occur in your IndyKite environment. Use Outbound Events when you need to: - Synchronize data with external systems in real-time. - Trigger workflows when specific data changes occur. - Audit and log operations performed on your IKG. - React to authorization or CIQ query executions. **Important:** Only one Outbound Events configuration can exist per Project. # How do Outbound Events work? The event flow consists of three stages: - **Configuration**: Define which event types to subscribe to (routes) and where to send them (providers). - **Event Generation**: When a matching operation occurs, IndyKite generates a message conforming to the CloudEvents standard. - **Event Publishing**: The event is delivered to the configured provider(s). After events are published, you are responsible for implementing any additional business logic for filtering and processing. # What providers are supported? IndyKite supports four event providers: - **Kafka** (Confluent Cloud or self-hosted) - **Azure Event Grid** - **Azure Service Bus** - **Google Cloud Pub/Sub** You can configure multiple providers and route different event types to different destinations. # How do I configure routing? An Outbound Events configuration has two main components: - **Providers**: The destinations where events will be sent (Kafka topic, Azure Event Grid, Azure Service Bus, Google Cloud Pub/Sub). - **Routes**: Rules that determine which events go to which providers. ### How does route ordering work? Routes are evaluated **sequentially in order**. An event can match multiple routes and be sent to multiple destinations. - If an event matches multiple routes, it is sent to all matching destinations. - Wildcard routes (e.g., `indykite.audit.capture.*`) catch all matching events. - Place specific routes **before** wildcard routes to ensure correct processing. ### What does the stop_processing flag do? The `stop_processing` flag controls whether to continue evaluating routes after a match: - `stop_processing = true`: Stop after this route matches. Event is not evaluated against further routes. - `stop_processing = false` (default): Continue matching. Event may be sent to multiple destinations. # What event types can I filter? ### Capture Events (Data Operations) **Operation** **Event Type** **Filters Available** BatchUpsertNodes `indykite.audit.capture.upsert.node` captureLabel, property key/value BatchUpsertRelationships `indykite.audit.capture.upsert.relationship` captureLabel, property key/value BatchDeleteNodes `indykite.audit.capture.delete.node` captureLabel, property key/value BatchDeleteRelationships `indykite.audit.capture.delete.relationship` captureLabel BatchDeleteNodeProperties `indykite.audit.capture.delete.node.property` — BatchDeleteRelationshipProperties `indykite.audit.capture.delete.relationship.property` — BatchDeleteNodeTags `indykite.audit.capture.delete.node.tag` captureLabel BatchDeleteNodePropertyMetadata `indykite.audit.capture.delete.node.property.metadata` — All Capture Events `indykite.audit.capture.*` Wildcard matches all ### Configuration Events **Operation** **Event Type** Create configuration `indykite.audit.config.create` Read configuration `indykite.audit.config.read` Update configuration `indykite.audit.config.update` Delete configuration `indykite.audit.config.delete` Assign permission `indykite.audit.config.permission.assign` Revoke permission `indykite.audit.config.permission.revoke` All Config Events `indykite.audit.config.*` ### Authorization and CIQ Events **Operation** **Event Type** Token Introspect `indykite.audit.credentials.token.introspected` AuthZEN Evaluation (`POST /access/v1/evaluation`) `indykite.audit.authorization.evaluation` AuthZEN Evaluations (`POST /access/v1/evaluations`) `indykite.audit.authorization.evaluations` AuthZEN Subject Search (`POST /access/v1/search/subject`) `indykite.audit.authorization.searchsubject` AuthZEN Resource Search (`POST /access/v1/search/resource`) `indykite.audit.authorization.searchresource` AuthZEN Action Search (`POST /access/v1/search/action`) `indykite.audit.authorization.searchaction` All Authorization Events `indykite.audit.authorization.*` CIQ Execute `indykite.audit.ciq.execute` # What are CDC events? Change Data Capture (CDC) events deliver the **actual content of a change** alongside the audit event. Instead of just signaling that a node was updated, the sink receives the full before and after state of the change, following the Neo4j CDC Output Schema. ### What is in a CDC payload? - **Event Type**: `node` or `relationship`. - **Operation**: `create`, `update`, or `delete`. - **Labels**: The labels of the node or relationship involved. - **Before and After states**: The property values before and after the change. CDC events carry their own event types, combining the entity with the operation: `indykite.audit.cdc.node.create`, `indykite.audit.cdc.node.update`, `indykite.audit.cdc.node.delete`, and the same three under `indykite.audit.cdc.relationship.*`. Filter them in routes like any other event type, for example with the `indykite.audit.cdc.*` wildcard. For example, a CDC event for an updated Person node shows that the `email` property changed from `old@example.com` to `new@example.com`, enabling a consuming system to correlate and act on the specific delta. ### How do I enable CDC events? CDC events are controlled per provider via the `include_cdc_events` boolean flag on the Event Sink configuration: - `include_cdc_events = true`: CDC events are emitted to the sink. - `include_cdc_events = false` or unset (default): CDC events are not emitted. # How do I configure Outbound Events? ## Example 1: Send all Capture events to Kafka **Goal:** Send an event to a Kafka topic each time a node or relationship is captured (upsert or delete). ### Using Terraform Documentation: indykite_event_sink resource resource "indykite_event_sink" "outbound_events" { name = "outbound-events" display_name = "Outbound Events" location = "gid:YOUR_PROJECT_GID" providers { provider_name = "confluent-provider" include_cdc_events = false kafka { brokers = ["pkc-xxxxx.region.gcp.confluent.cloud:9092"] topic = "topic_signal" username = "" password = "" } } routes { provider_id = "confluent-provider" route_id = "capture-events" route_display_name = "Capture Events" stop_processing = true keys_values_filter { event_type = "indykite.audit.capture.*" } } } ### Using REST API Endpoint: POST /event-sinks { "project_id": "YOUR_PROJECT_GID", "name": "outbound-events", "display_name": "Outbound Events", "description": "Capture events to Kafka", "providers": { "confluent-provider": { "include_cdc_events": false, "kafka": { "brokers": ["pkc-xxxxx.region.gcp.confluent.cloud:9092"], "topic": "topic_signal", "username": "", "password": "", "disable_tls": false, "tls_skip_verify": false } } }, "routes": [ { "provider_id": "confluent-provider", "route_id": "capture-events", "display_name": "Capture Events", "stop_processing": true, "event_type_key_values_filter": { "event_type": "indykite.audit.capture.*" } } ] } ## Example 2: Filter events by node label and property **Goal:** Send events only when a Person node with an email property is upserted. ### Using Terraform routes { provider_id = "kafka-provider" route_id = "person-email-events" route_display_name = "Person Email Events" stop_processing = true keys_values_filter { event_type = "indykite.audit.capture.upsert.node" key_value_pairs { key = "captureLabel" value = "Person" } key_value_pairs { key = "email" value = "*" } } } ### Using REST API { "provider_id": "kafka-provider", "route_id": "person-email-events", "display_name": "Person Email Events", "stop_processing": true, "event_type_key_values_filter": { "event_type": "indykite.audit.capture.upsert.node", "context_key_value": [ { "key": "captureLabel", "value": "Person" }, { "key": "email", "value": "*" } ] } } ## Example 3: Route to multiple providers **Goal:** Route different event types to different providers: - Person node captures → Kafka - CIQ executions → Azure Event Grid - Config changes → Azure Service Bus ### Using Terraform resource "indykite_event_sink" "multi_provider" { name = "multi-provider-events" display_name = "Multi Provider Events" location = "gid:YOUR_PROJECT_GID" providers { provider_name = "kafka-provider" include_cdc_events = false kafka { brokers = ["broker1:9092", "broker2:9092"] topic = "capture-events" username = "" password = "" } } providers { provider_name = "azure-grid-provider" include_cdc_events = false azure_event_grid { topic_endpoint = "https://your-topic.eventgrid.azure.net/api/events" access_key = "" } } providers { provider_name = "azure-bus-provider" include_cdc_events = false azure_service_bus { connection_string = "Endpoint=sb://your-namespace.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=" queue_or_topic_name = "config-events" } } routes { provider_id = "kafka-provider" route_id = "person-captures" route_display_name = "Person Captures" stop_processing = true keys_values_filter { event_type = "indykite.audit.capture.upsert.node" key_value_pairs { key = "captureLabel" value = "Person" } } } routes { provider_id = "azure-grid-provider" route_id = "ciq-executions" route_display_name = "CIQ Executions" stop_processing = true keys_values_filter { event_type = "indykite.audit.ciq.execute" } } routes { provider_id = "azure-bus-provider" route_id = "config-changes" route_display_name = "Config Changes" stop_processing = true keys_values_filter { event_type = "indykite.audit.config.*" } } } ## Example 4: Send events to Google Cloud Pub/Sub **Goal:** Publish events to a Google Cloud Pub/Sub topic using a GCP service account. The `pubsub` provider requires three fields: - `project_id`: The GCP project ID that owns the topic (6–30 characters). - `topic_name`: The Pub/Sub topic name (3–255 characters). - `credentials_json`: The full JSON of a GCP service account key with permission to publish to the topic (e.g. `roles/pubsub.publisher`). ### Using Terraform resource "indykite_event_sink" "pubsub_events" { name = "pubsub-outbound-events" display_name = "Pub/Sub Outbound Events" location = "gid:YOUR_PROJECT_GID" providers { provider_name = "pubsub-provider" include_cdc_events = false pubsub { project_id = "my-gcp-project" topic_name = "my-pubsub-topic" credentials_json = file("${path.module}/service-account.json") } } routes { provider_id = "pubsub-provider" route_id = "capture-events" route_display_name = "Capture Events" stop_processing = true keys_values_filter { event_type = "indykite.audit.capture.*" } } } ### Using REST API { "project_id": "YOUR_PROJECT_GID", "name": "pubsub-outbound-events", "display_name": "Pub/Sub Outbound Events", "description": "Capture events to Google Cloud Pub/Sub", "providers": { "pubsub-provider": { "include_cdc_events": false, "pubsub": { "project_id": "my-gcp-project", "topic_name": "my-pubsub-topic", "credentials_json": "{\"type\":\"service_account\",...}" } } }, "routes": [ { "provider_id": "pubsub-provider", "route_id": "capture-events", "display_name": "Capture Events", "stop_processing": true, "event_type_key_values_filter": { "event_type": "indykite.audit.capture.*" } } ] } **Security note:** The `credentials_json` value is a sensitive secret. Store it outside of version control (for example, in Terraform Cloud variables, Vault, or a CI secret store) and load it at apply time. # How do I filter by multiple labels? To match nodes with multiple labels, add multiple `captureLabel` key-value pairs: { "key": "captureLabel", "value": "Person" }, { "key": "captureLabel", "value": "Human" } For this filter to match, the captured node must have: - `"type": "Person"` - `"tags": ["Human"]` # What are Kafka brokers? A Kafka Broker is a server that receives, stores, and distributes messages between producers and consumers. ### Why specify multiple brokers? The primary reason is **resilience**. If the first broker is unavailable, the client tries the next one in the array. Best practice: Include at least two brokers to ensure connectivity even if one broker is down. `"brokers": ["broker1.confluent.cloud:9092", "broker2.confluent.cloud:9092"]` # How do TLS settings work? ### What does disable_tls do? Setting `disable_tls = true` connects to Kafka without encryption (plain TCP). **Setting** **TLS Enabled (default)** **TLS Disabled** Data encryption Encrypted in transit Unencrypted (vulnerable to interception) Authentication Certificate-based verification None (unless using SASL) Performance Slight overhead Slightly faster Security Production-ready Development only ### What does tls_skip_verify do? Setting `tls_skip_verify = true` skips certificate validation while still encrypting data. **Setting** **Verify Enabled (default)** **Verify Skipped** Data encryption Encrypted Encrypted Server authentication Certificate validated Certificate accepted without validation MITM protection Protected Vulnerable Use case Production Development with self-signed certs **Recommendation:** Only disable TLS or skip verification in development environments or highly secure private networks. # Verification After configuring Outbound Events: - Perform an operation that matches your route (e.g., capture a node). - Check your provider (Kafka topic, Azure Event Grid, Azure Service Bus, Google Cloud Pub/Sub) for the event message. - Verify the message conforms to the CloudEvents standard. # Next Steps - Terraform examples: Terraform Configurations - REST API reference: Event Sink API - Full examples: Developer Hub Resources --- Source: https://developer.indykite.com/guides/guide-outbound-events --- # IndyKite Products Overview > A comprehensive guide to IndyKite products, their capabilities, and how they work together. **Category:** Overview ## Summary Understand IndyKite products and their relationships for AI agents and developers. ## Content # What is IndyKite? IndyKite is a **real-time data retrieval and authorization platform** built on a Knowledge Graph. It enables applications to store identity data, query it with built-in authorization, and make fine-grained access control decisions. ## What problems does IndyKite solve? - **Fragmented identity data**: Unify data from multiple sources into a single graph. - **Coarse-grained authorization**: Move beyond roles to KBAC (Knowledge-Based Access Control). - **Static access rules**: Enable real-time, context-aware authorization decisions. - **Data quality uncertainty**: Assess and score data trustworthiness. - **AI agent authorization**: Provide guardrails for AI systems accessing sensitive data. # What are the IndyKite products? Product Purpose API Endpoint Identity Knowledge Graph (IKG) Store nodes and relationships /capture/v1/* ContX IQ (CIQ) Context-aware data queries /contx-iq/v1/execute KBAC Authorization decisions /access/v1/evaluation Token Introspect Validate tokens, map to IKG /configs/v1/token-introspects External Data Resolver Fetch external data at query time /configs/v1/external-data-resolvers Trust Score Assess data quality /configs/v1/trust-score-profiles Outbound Events Push notifications on data changes /configs/v1/event-sinks # How do the products relate to each other? The products form a layered architecture: # What is the Identity Knowledge Graph (IKG)? The IKG is a **Neo4j graph database** that stores your identity and resource data as nodes (entities) and relationships (connections). ### What can I store in the IKG? - **Nodes**: People, Organizations, Devices, Resources, Contracts, Vehicles, etc. - **Relationships**: OWNS, WORKS_FOR, HAS_ACCESS, ACCEPTED, COVERS, etc. - **Properties**: Attributes on nodes and relationships (email, name, status, etc.) ### How do I add data to the IKG? Use the **Capture API** to ingest nodes and relationships: - `POST /capture/v1/nodes` - Create or update nodes - `POST /capture/v1/relationships` - Create relationships between nodes Authentication: **Application Agent credentials** (X-IK-ClientKey header) See: Capture API Example # What is ContX IQ (CIQ)? ContX IQ (Contextual Intelligence Query) is a **context-aware query engine**. It retrieves data from the Knowledge Graph while enforcing authorization policies based on the context at query time. ### What makes CIQ context-aware? - **Query-time evaluation**: Authorization decisions are made when the query executes, not pre-computed. - **Caller context**: Considers the caller's identity, relationships, and current state. - **Current graph state**: Queries reflect the graph as it exists at query time. - **Parameter binding**: Pass context parameters (e.g., user email, session data) to filter queries. ### What can CIQ do? - **Contextual reads**: Query nodes and relationships based on the caller's real-time context. - **Authorized writes**: Update data only when the current context permits. - **External data integration**: Fetch real-time data from external APIs via External Data Resolver. ### How does CIQ work? - Create an **Authorization Policy** defining context-based access rules. - Create a **Knowledge Query** linked to the policy. - Execute the query with `POST /contx-iq/v1/execute` passing runtime context. - CIQ evaluates the context at query time and returns only authorized data. Authentication: **Application Agent credentials** (X-IK-ClientKey header) See: ContX IQ Guide # What is KBAC? KBAC (Knowledge-Based Access Control) is an **authorization decision engine**. It answers the question: "Can this subject perform this action on this resource?" ### How is KBAC different from CIQ? Aspect ContX IQ (CIQ) KBAC Purpose Query and retrieve data Make allow/deny decisions Response Data results (nodes, properties) ALLOW or DENY Use case Fetch authorized data Check if action is permitted Policy type 1.0-ciq (read/write operations) 2.0-kbac, or 3.0-kbac for raw Cypher with optional data-residency routing (action-based) ### When should I use KBAC? - Checking if a user can perform an action before executing it. - API gateway authorization. - AI agent guardrails (can this agent access this resource?). See: AuthZEN Guide | Dynamic Authorization Guide # What is Token Introspect? Token Introspect **validates access tokens** from external identity providers and **maps them to nodes** in your IKG. ### How does Token Introspect work? - Your app receives an access token from an identity provider (Auth0, Okta, etc.). - Call `POST /identity/v1/introspect` with the token. - IndyKite validates the token and extracts claims (e.g., email). - IndyKite matches the claim to a node in your IKG (e.g., Person with that email). - Returns the matched node data enriched with IKG relationships. ### When should I use Token Introspect? - Enriching identity provider tokens with IKG data. - Bridging external identity systems to your knowledge graph. - Getting a Person node from a JWT claim. See: Token Introspect Terraform # What is External Data Resolver? External Data Resolver **fetches data from external APIs** during CIQ query execution and makes it available as node properties. ### How does it work? - Configure an External Data Resolver with an API endpoint. - In your IKG, create a node property with `external_value` pointing to the resolver. - When CIQ queries that property, it calls the external API in real-time. - The API response is returned as if it were stored in the IKG. ### When should I use External Data Resolver? - Data that changes frequently and shouldn't be stored. - Data from external systems (CRM, ERP, etc.). - Calculated or derived values. See: External Data Resolver Guide # What is Trust Score? Trust Score **assesses data quality** based on configurable dimensions and attaches a score to nodes in the IKG. ### What dimensions affect Trust Score? Dimension What it measures FRESHNESS How recently the data was updated ORIGIN Source system credibility VALIDITY Whether data is within valid date ranges COMPLETENESS Percentage of required properties present VERIFICATION Whether data has been verified ### How can I use Trust Score in authorization? Reference the `_TrustScore` node in your KBAC or CIQ policies: - Only allow access if trust score > 0.8 - Require higher scores for sensitive data - Filter query results by data quality See: Trust Score Guide # What are Outbound Events? Outbound Events **push real-time notifications** to external systems when data changes in your IKG or configurations are modified. ### What events can I capture? - **Capture events**: Node/relationship created, updated, or deleted. - **Config events**: Policy, query, or configuration changes. ### Where can events be sent? - Kafka (Confluent Cloud) - Azure Event Grid - Azure Service Bus ### When should I use Outbound Events? - Triggering workflows when data changes. - Audit logging of all modifications. - Synchronizing data with external systems. See: Outbound Events Guide | Kafka Terraform # What credentials do I need? Credential Type Created At Used For Header Service Account Organization level Config API (create policies, queries, projects) Authorization: Bearer {token} Application Agent Application level Capture API, CIQ, KBAC, Token Introspect X-IK-ClientKey: {credential} See: Credentials Guide # How do I get started? ### Step 1: Create environment - Create a Sandbox account at IndyKite Hub - Create Service Account credentials - Create a Project with an IKG - Create an Application and Application Agent ### Step 2: Capture data - Use the Capture API to ingest nodes and relationships - See: Capture API Example ### Step 3: Create policies - Create Authorization Policies defining access rules - Create Knowledge Queries for CIQ ### Step 4: Query and authorize - Use CIQ to query data with authorization - Use KBAC for allow/deny decisions # Quick Reference for AI Agents ### Common workflows **Query authorized data:** 1. POST /capture/v1/nodes (ingest data) 2. POST /configs/v1/authorization-policies (create policy) 3. POST /configs/v1/knowledge-queries (create query) 4. POST /contx-iq/v1/execute (run query) **Check authorization:** 1. POST /capture/v1/nodes (ingest data) 2. POST /configs/v1/authorization-policies (create KBAC policy) 3. POST /kbac/v1/is-authorized (check permission) **Validate external token:** 1. POST /configs/v1/token-introspects (configure) 2. POST /identity/v1/introspect (validate token) ### API base URLs - EU: `https://eu.api.indykite.com` - US: `https://us.api.indykite.com` ### Machine-readable resources - OpenAPI: https://openapi.indykite.com - Resource index: /api/resources.json - Terraform index: /api/terraform.json - AI sitemap: /llms.txt ### Next steps - Sandbox guide: Get started with the Sandbox - Environment setup: Environment Guide - Code examples: Developer Resources - Terraform configs: Terraform Examples --- Source: https://developer.indykite.com/guides/guide-products --- # Get started with the Sandbox > How to use the IndyKite platform: connect your data, create contextually relevant queries and apply authorization policies. **Category:** Environment ## Summary Getting started with the IndyKite platform using the Sandbox environment. ## Content **What is IndyKite?** IndyKite is a real-time data retrieval and enforcement layer purpose-built to bring deep context, granular control, and usability to your enterprise data. Core capabilities: - **Capture**: Store nodes and relationships in the Identity Knowledge Graph (IKG). - **ContX IQ (CIQ)**: Context-aware queries with built-in authorization. - **KBAC**: Knowledge-Based Access Control for fine-grained authorization policies. - **External Data Resolver**: Fetch data from external APIs in real-time during query execution. - **Trust Score**: Assess data quality based on freshness, origin, and verification. - **Outbound Events**: Push real-time notifications to external systems when data changes. This Quick Start Guide walks you through: - 1. Creating a Sandbox account and accessing the platform. - 2. Setting up your environment (credentials, project, application). - 3. Capturing data into your Identity Knowledge Graph. - 4. Using IndyKite products: CIQ, KBAC, and Outbound Events. # Step 1: Access the Platform **Register for the IndyKite Sandbox** The Sandbox is a free environment where you can explore IndyKite features. Create a sandbox account in the IndyKite Hub: - EU Region: https://eu.hub.indykite.com - US Region: https://us.hub.indykite.com Create an account: IndyKite Sandbox Registration Redirection to the IndyKite Hub: # Step 2: Create Your Environment IndyKite uses a hierarchical structure for organizing resources: - **Organization**: Your top-level account container. - **Service Account**: Credentials for managing configurations via the Config API. - **Project**: An isolated working environment with its own Identity Knowledge Graph (IKG). - **Application**: Represents your software system within a project. - **Application Agent**: Identity that authenticates API calls from your application. ## Option A: Quick Start Script (Recommended) The fastest way to set up your environment is using the Developer Hub quick start script: https://github.com/indykite/developer-hub/tree/master/get-started This script will: - Create a project and application with credentials. - Capture sample data into your IKG. - Create KBAC and CIQ policies. - Run tests to verify the setup. Prerequisites: Organization ID and Service Account credentials from the Hub. ## Option B: Manual Setup via Hub **2.1 Create Service Account Credentials** Service Account credentials are required for the Config API (creating configurations, projects, applications). Go to: https://eu.hub.indykite.com/service-accounts Create a new Service Account: Download the generated credentials JSON file: **2.2 Create a Project** A Project is an isolated environment with its own Identity Knowledge Graph (IKG). The IKG is a Neo4j graph database instance. If you need a Neo4j instance, you can get one free at: - Neo4j Aura Console - Neo4j AuraDB - Neo4j Desktop Create a new project in the Hub: Enter your Neo4j connection details: **2.3 Create Application and Application Agent** An Application represents your software system. The Application Agent provides credentials for API authentication. You can create these in the Hub UI, REST API: OpenAPI documentation or use Terraform: Environment Configuration with Terraform ## Option C: Setup via Terraform Use Terraform for infrastructure-as-code setup: - Project, Application, Application Agent: terraform-2 - Token Introspect (for external identity providers): terraform-1 # Step 3: Capture Data The IndyKite platform operates on data stored in your Identity Knowledge Graph (IKG). Before using CIQ or KBAC, you must capture data into your graph. **What is Capture?** Capture is the process of storing nodes and relationships in your IKG. Data must be transformed into the schema accepted by the Capture API endpoints. API Documentation: Capture API Reference **Examples and Resources:** - Developer Hub Resources: https://developer.indykite.com/resources - GitHub Examples: https://github.com/indykite/developer-hub **Visualize Your Data:** After capturing data, view it in the Data Explorer: https://eu.hub.indykite.com/explore/data-explorer # Step 4: Use IndyKite Products ## ContextIQ (CIQ) **What is CIQ?** ContextIQ is the primary API for interacting with data in your IKG. It provides authorized Read, Update, and Delete operations on graph data. Key features: - Query graph data with contextual relevance. - Make real-time updates to nodes and relationships. - Automatic data protection through KBAC policies. Guide: CIQ Guide Examples: Developer Hub Resources ## KBAC / AuthZEN **What is KBAC?** Knowledge-Based Access Control (KBAC) is an OpenID AuthZEN-compliant authorization engine. It automatically authorizes access to all data in your IKG based on policies you define. Key features: - Powers CIQ authorization automatically. - Can be invoked directly via the AuthZEN API. - Fine-grained, relationship-aware access control. OpenID AuthZEN specification: https://openid.net/wg/authzen/ Guides: - Dynamic Authorization Guide - AuthZEN Guide Examples: Developer Hub Resources ## External Data Resolver **What is External Data Resolver?** External Data Resolver (Data Reference) enables fetching data from external APIs in real-time during CIQ query execution. Store sensitive data externally while still querying it through IndyKite. Key features: - Real-time external API lookups during queries. - Keep sensitive data (VINs, SSNs) in secure external systems. - Combine IKG graph data with external data sources. Guide: External Data Resolver Guide ## Trust Score **What is Trust Score?** Trust Score assesses data quality based on configurable dimensions like freshness, origin, and verification status. Use trust scores in authorization decisions. Key features: - Evaluate data trustworthiness automatically. - Configure dimensions: Freshness, Origin, Validity, Completeness, Verification. - Use trust scores in KBAC policies and CIQ queries. Guide: Trust Score Guide ## Outbound Events / Signals **What are Outbound Events?** Outbound Events (Signals) push real-time notifications to external systems when data changes or events occur in your IndyKite environment. Supported providers: - Kafka (Confluent) - Azure Event Grid - Azure Service Bus Guide: Outbound Events Guide Examples: - Developer Hub Resources - Terraform Configurations # Videos # Additional Resources - **Developer Hub**: https://developer.indykite.com/ - **REST API Reference**: https://openapi.indykite.com - **Terraform Provider**: IndyKite Terraform Provider - **Documentation**: https://docs.indykite.com - **GitHub Examples**: https://github.com/indykite/developer-hub - **Community Forum**: https://forum.indykite.com/ - **Graph Modeling Tool**: Arrows.app (Neo4j graph modeling) --- Source: https://developer.indykite.com/guides/guide-sandbox --- # Use IndyKite from your AI coding agent > Install the IndyKite skills bundle so Claude Code, Gemini CLI, and other coding agents can author ContX IQ policies, call the MCP server, and deploy Agent Gateway against your project - from a single prompt. **Category:** MCP ## Summary Install the IndyKite skills bundle and drive ContX IQ, MCP, and Agent Gateway from natural-language prompts in Claude Code, Gemini CLI, and other agents. ## Content The [`indykite/skills`](https://github.com/indykite/skills) repo packages IndyKite's core developer workflows as **skills** - bundles of instructions a coding agent loads on demand. After install, your agent can author CIQ policies, initialise MCP sessions, and deploy IAG against your project from a single prompt. Verified end-to-end with **Claude Code** and **Gemini CLI**. The [`skills`](https://skills.sh) CLI also drops files into the right place for Cursor, Aider, Continue, and any other agent it supports - automatic activation in those agents depends on the agent. ## What your agent can do after install Each row is a real prompt the matching skill is designed to handle. After install, paste the prompt into your agent - the skill activates automatically based on the prompt's wording. | You ask your agent… | Skill that handles it | | --- | --- | | "Expose `Person`-`OWNS`-`Car` as a parameterised read query." | [`indykite-ciq-read`](https://github.com/indykite/skills/tree/main/indykite-ciq-read) | | "Create a new `Track` node in the IKG with `title` and `loudness`." | [`indykite-ciq-create-node`](https://github.com/indykite/skills/tree/main/indykite-ciq-create-node) | | "Add a `PLAYED_AT` relationship between an existing `Track` and `Venue`." | [`indykite-ciq-create-relationship`](https://github.com/indykite/skills/tree/main/indykite-ciq-create-relationship) | | "Create a new `Contract` and atomically link it to an existing `Vehicle` and `Person`." | [`indykite-ciq-create-node-with-link`](https://github.com/indykite/skills/tree/main/indykite-ciq-create-node-with-link) | | "Let a `Person` update their own `music_mood` property." | [`indykite-ciq-add-property`](https://github.com/indykite/skills/tree/main/indykite-ciq-add-property) | | "Annotate a `PLAYED_AT` relationship with a `verified` flag." | [`indykite-ciq-add-relationship-property`](https://github.com/indykite/skills/tree/main/indykite-ciq-add-relationship-property) | | "Clear the `music_mood` property from a `Person` - GDPR erase." | [`indykite-ciq-delete`](https://github.com/indykite/skills/tree/main/indykite-ciq-delete) | | "Author a KBAC policy letting a `Person` `PROVISION` a `Server` only when they are `MEMBER_OF` a `Team` that `OWNS` it, publish it as ACTIVE, then deactivate the old provisioning policy." | [`indykite-authzen-kbac-policies`](https://github.com/indykite/skills/tree/main/indykite-authzen-kbac-policies) | | "Can `ada` `PROVISION` `gpu-node-7` with `max_budget` 120000 passed as an input param? Gate the deploy step on the live decision and explain a `false`." | [`indykite-authzen-evaluation`](https://github.com/indykite/skills/tree/main/indykite-authzen-evaluation) | | "In one batch call, check for `ada`, `grace`, and `linus` which of `DEPLOY` and `RESTART` each may perform on `gpu-node-7`, and give me the allow/deny grid." | [`indykite-authzen-evaluations`](https://github.com/indykite/skills/tree/main/indykite-authzen-evaluations) | | "Which actions is `linus` allowed to perform on `gpu-node-7`? I want to render only the permitted buttons in the admin UI." | [`indykite-authzen-search-action`](https://github.com/indykite/skills/tree/main/indykite-authzen-search-action) | | "List every `Server` that `ada` can `PROVISION` so we can prefill the target dropdown with only her permitted machines." | [`indykite-authzen-search-resource`](https://github.com/indykite/skills/tree/main/indykite-authzen-search-resource) | | "Who can `APPROVE` the document `contract-2043`? Produce the reviewer list for the quarterly access audit." | [`indykite-authzen-search-subject`](https://github.com/indykite/skills/tree/main/indykite-authzen-search-subject) | | "Build the Capture payload to ingest three `Person` employees with verified `email` properties - source, assurance level, and verification time metadata - routing `emma` to the `east` location." | [`indykite-capture-upsert-nodes`](https://github.com/indykite/skills/tree/main/indykite-capture-upsert-nodes) | | "Link each imported `Person` to their `Department` with a `MEMBER_OF` relationship carrying a `since` property; the cross-location edges go to the global database." | [`indykite-capture-upsert-relationships`](https://github.com/indykite/skills/tree/main/indykite-capture-upsert-relationships) | | "Prepare the delete payload that removes all the seeded `qa-*` test `Person` nodes from the IKG in one batch." | [`indykite-capture-delete-nodes`](https://github.com/indykite/skills/tree/main/indykite-capture-delete-nodes) | | "GDPR erasure request: strip the `email` and `phone` properties from `person-millicent` but keep the node and its relationships intact." | [`indykite-capture-delete-node-properties`](https://github.com/indykite/skills/tree/main/indykite-capture-delete-node-properties) | | "Remove the `assurance_level` and `verified_time` metadata from `millicent`'s `email` property - the value stays, the provenance goes." | [`indykite-capture-delete-node-property-metadata`](https://github.com/indykite/skills/tree/main/indykite-capture-delete-node-property-metadata) | | "Contract ended: delete the `CAN_DRIVE` relationship between `ryan` and `kitt` without touching either node." | [`indykite-capture-delete-relationships`](https://github.com/indykite/skills/tree/main/indykite-capture-delete-relationships) | | "Drop the `status` and `renewal_date` properties from the `OWNS` edge between `knightrider` and `kitt`; the relationship itself must survive." | [`indykite-capture-delete-relationship-properties`](https://github.com/indykite/skills/tree/main/indykite-capture-delete-relationship-properties) | | "Initialise an MCP session against `eu.mcp.indykite.com` and call `authzen_evaluate`." | [`indykite-mcp-server`](https://github.com/indykite/skills/tree/main/indykite-mcp-server) | | "Deploy IAG in front of my three A2A agents and wire up the workflow in the IKG." | [`indykite-agent-gateway`](https://github.com/indykite/skills/tree/main/indykite-agent-gateway) | ## Full skill catalog (hosted on this site) Every skill is also hosted on this site, verbatim. If your agent cannot install skills, point it at a skill's `SKILL.md` URL below (or at the machine-readable index at [/api/skills.json](/api/skills.json)) and it can follow the instructions in-context — the relative links to each skill's `references/` and `scripts/` resolve on this site too. 23 skills, served verbatim from https://github.com/indykite/skills. Each skill is a self-contained instruction bundle: fetch its SKILL.md and follow the relative links (references/, scripts/) for the full workflow. Machine index: /api/skills.json. Install into a coding agent with `npx skills add indykite/skills`. ### Agent Gateway | Skill | What it does | | --- | --- | | [indykite-agent-gateway](/agent-skills/indykite-agent-gateway/SKILL.md) | Deploy and configure IndyKite Agent Gateway (IAG) in front of agent-to-agent (A2A) workflows or MCP servers. | ### AuthZEN / KBAC | Skill | What it does | | --- | --- | | [indykite-authzen-evaluation](/agent-skills/indykite-authzen-evaluation/SKILL.md) | Make a single KBAC authorization decision via the IndyKite AuthZEN REST API (`POST /access/v1/evaluation`) - returns a boolean `decision` for one (subject, action, resource) triple, optionally with per-request... | | [indykite-authzen-evaluations](/agent-skills/indykite-authzen-evaluations/SKILL.md) | Run many KBAC authorization decisions in one call via the IndyKite AuthZEN REST API (`POST /access/v1/evaluations`), with top-level subject/action/resource/context as defaults overridden per entry; returns one... | | [indykite-authzen-kbac-policies](/agent-skills/indykite-authzen-kbac-policies/SKILL.md) | Author and manage an IndyKite KBAC (Knowledge-Based Access Control) authorization policy - a single subject type, an actions list, a single resource type, and a Cypher condition over the IKG - through the Config API... | | [indykite-authzen-search-action](/agent-skills/indykite-authzen-search-action/SKILL.md) | List the actions a subject is allowed to perform on a resource via the IndyKite AuthZEN REST API (`POST /access/v1/search/action`) - returns the granted action names for one pinned (subject, resource) pair. | | [indykite-authzen-search-resource](/agent-skills/indykite-authzen-search-resource/SKILL.md) | List the resources a subject is allowed to perform a given action on via the IndyKite AuthZEN REST API (`POST /access/v1/search/resource`) - given a subject and an action, returns the matching resource instances of a... | | [indykite-authzen-search-subject](/agent-skills/indykite-authzen-search-subject/SKILL.md) | List the subjects allowed to perform a given action on a resource via the IndyKite AuthZEN REST API (`POST /access/v1/search/subject`) - given a resource and an action, returns the matching subject instances of a type. | ### Capture API | Skill | What it does | | --- | --- | | [indykite-capture-delete-node-properties](/agent-skills/indykite-capture-delete-node-properties/SKILL.md) | Build the request-body JSON for the IndyKite Capture API batch node-property delete (`POST /capture/v1/nodes/properties/delete`) - a `nodes` array (1-250 per request) where each entry names a node (`external_id` +... | | [indykite-capture-delete-node-property-metadata](/agent-skills/indykite-capture-delete-node-property-metadata/SKILL.md) | Build the request-body JSON for the IndyKite Capture API batch property-metadata delete (`POST /capture/v1/nodes/properties/metadata/delete`) - a `nodes` array (1-250 per request) where each entry names a node... | | [indykite-capture-delete-nodes](/agent-skills/indykite-capture-delete-nodes/SKILL.md) | Build the request-body JSON for the IndyKite Capture API batch node delete (`POST /capture/v1/nodes/delete`) - a `nodes` array (1-250 per request) of `{external_id, type}` references, each removing one whole node... | | [indykite-capture-delete-relationship-properties](/agent-skills/indykite-capture-delete-relationship-properties/SKILL.md) | Build the request-body JSON for the IndyKite Capture API batch relationship-property delete (`POST /capture/v1/relationships/properties/delete`) - a `relationships` array (1-250 per request), each entry identifying a... | | [indykite-capture-delete-relationships](/agent-skills/indykite-capture-delete-relationships/SKILL.md) | Build the request-body JSON for the IndyKite Capture API batch relationship delete (`POST /capture/v1/relationships/delete`) - a `relationships` array (1-250 per request), each entry identifying a relationship by... | | [indykite-capture-upsert-nodes](/agent-skills/indykite-capture-upsert-nodes/SKILL.md) | Build the request-body JSON for the IndyKite Capture API batch node upsert (`POST /capture/v1/nodes`) - a `nodes` array (1-250 per request) of entities, each with `external_id`, `type`, optional `is_identity` /... | | [indykite-capture-upsert-relationships](/agent-skills/indykite-capture-upsert-relationships/SKILL.md) | Build the request-body JSON for the IndyKite Capture API batch relationship upsert (`POST /capture/v1/relationships`) - a `relationships` array (1-250 per request), each entry connecting a `source` node to a `target`... | ### ContX IQ | Skill | What it does | | --- | --- | | [indykite-ciq-add-property](/agent-skills/indykite-ciq-add-property/SKILL.md) | Author an IndyKite ContX IQ (CIQ) policy plus its Knowledge Query that sets one or more properties on an existing node in the IndyKite Graph (IKG), then run it via `POST /contx-iq/v1/execute`. | | [indykite-ciq-add-relationship-property](/agent-skills/indykite-ciq-add-relationship-property/SKILL.md) | 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`. | | [indykite-ciq-create-node](/agent-skills/indykite-ciq-create-node/SKILL.md) | Author an IndyKite ContX IQ (CIQ) policy plus its Knowledge Query that creates a brand-new node in the IndyKite Graph (IKG), then run it via `POST /contx-iq/v1/execute`. | | [indykite-ciq-create-node-with-link](/agent-skills/indykite-ciq-create-node-with-link/SKILL.md) | Author an IndyKite ContX IQ (CIQ) policy plus its Knowledge Query that creates a brand-new node AND links it to one or more existing nodes via new relationships in a single `POST /contx-iq/v1/execute` call. | | [indykite-ciq-create-relationship](/agent-skills/indykite-ciq-create-relationship/SKILL.md) | Author an IndyKite ContX IQ (CIQ) policy plus its Knowledge Query that creates a brand-new relationship between two existing nodes in the IndyKite Graph (IKG), then run it via `POST /contx-iq/v1/execute`. | | [indykite-ciq-delete](/agent-skills/indykite-ciq-delete/SKILL.md) | Author an IndyKite ContX IQ (CIQ) policy plus its Knowledge Query that deletes a node, a relationship, or one or more properties from the IndyKite Graph (IKG), then run it via `POST /contx-iq/v1/execute`. | | [indykite-ciq-read](/agent-skills/indykite-ciq-read/SKILL.md) | Author a read-only IndyKite ContX IQ (CIQ) policy plus its Knowledge Query, then run it via `POST /contx-iq/v1/execute`. | ### Other | Skill | What it does | | --- | --- | | [indykite-data-schema](/agent-skills/indykite-data-schema/SKILL.md) | Read the observed data schema of the IndyKite Knowledge Graph (IKG) via the Data Schema REST API (`GET /data-schema/v1/`) - a JGFv2 document listing every node type with its properties, value-type tallies, and... | ### MCP Server | Skill | What it does | | --- | --- | | [indykite-mcp-server](/agent-skills/indykite-mcp-server/SKILL.md) | Make live IndyKite authorization decisions (AuthZEN/KBAC) and run ContX IQ graph queries from an AI agent over the Model Context Protocol - self-contained Bearer-token JSON-RPC calls on the stateless protocol... | ## Prerequisites - An IndyKite project. Create one with `POST /configs/v1/projects` or via the [IndyKite Terraform provider](https://registry.terraform.io/providers/indykite/indykite/latest/docs). - A coding agent - Claude Code, Gemini CLI, or any other agent the [`skills`](https://skills.sh) CLI supports. - Node.js on your `PATH` so `npx` can run the CLI. ## Install One line, into every agent the CLI detects on your machine: ```bash npx skills add indykite/skills ``` Restart the agent so it picks up the new skill directory, then verify: ```bash npx skills list ``` ### Cherry-pick a single skill If you only need one capability - say CIQ reads - install just that one: ```bash npx skills add indykite/skills --skill indykite-ciq-read --agent claude-code ``` ### Bundle install (Claude Code or Gemini CLI) For agents with a plugin marketplace, the bundle registers every skill at once and prompts for credentials at install time. Claude Code: ```text /plugin marketplace add indykite/skills /plugin install indykite-skills ``` Gemini CLI: ```bash gemini extensions install https://github.com/indykite/skills ``` ## Try it: your first prompt After install + restart, paste this into your agent: > Initialise an MCP session against `eu.mcp.indykite.com` for my project and list the available tools. The `indykite-mcp-server` skill should activate and walk the agent through `mcp/initialize`, header construction, and the `tools/list` call. If it doesn't activate, see **Troubleshooting** below. ## Credentials Skills read connection details from environment variables - or, if you used **Bundle install**, from the prompts you answered at install time. You'll need a subset depending on which skills you use: | **Variable** | **What it is** | **Where to get it** | | --- | --- | --- | | `API_URL` | IndyKite REST base URL for your region | [https://eu.api.indykite.com](https://eu.api.indykite.com) or [https://us.api.indykite.com](https://us.api.indykite.com) | | `API_KEY` | AppAgent token (sent as `X-IK-ClientKey`) | `POST /configs/v1/application-agent-credentials`, or `indykite_application_agent_credential` in the Terraform provider | | `BEARER_TOKEN` | User OAuth access token | Standard IndyKite OAuth flow. Required for Person-subject CIQ flows and MCP session init | | `SERVICE_ACCOUNT_TOKEN` | Service-account token with Config API write access | From Hub UI, through the Config REST API or the Terraform provider. | | `MCP_URL` | IndyKite MCP base URL | [https://eu.mcp.indykite.com](https://eu.mcp.indykite.com) or [https://us.mcp.indykite.com](https://us.mcp.indykite.com). Used by `indykite-mcp-server` only | | `PROJECT_GID` | IndyKite project identifier | Returned in the response of `POST /configs/v1/projects`, or fetch later via `GET /configs/v1/projects` | `API_KEY`, `BEARER_TOKEN`, and `SERVICE_ACCOUNT_TOKEN` are secrets. Don't commit them, don't paste them into chat transcripts, and scope them down where possible. ## Troubleshooting ### A skill doesn't activate 1. **It's installed.** Run `npx skills list`. If the skill name isn't there, reinstall with `--agent `. 2. **The prompt fits the description.** Each `SKILL.md` has a `description` and `## When to use`. If your prompt is vague, rephrase it toward the wording there. 3. **No conflicting skill.** If two skills could plausibly handle the same prompt, the agent picks one. Disambiguate by invoking the skill explicitly - `/` in Claude Code, or your agent's equivalent. ### A CIQ or MCP call returns 401 / 403 - `API_KEY` is the AppAgent token, **not** a user token. - `BEARER_TOKEN` (when required) is fresh - IndyKite OAuth tokens are short-lived. - `PROJECT_GID` matches the project the credentials belong to. The `indykite-mcp-server` skill has a dedicated debugging path for MCP 401s; ask the agent about it directly. ## Updating to a new release `npx skills add` installs symlinks by default, so a `git pull` in your local copy (or rerunning the install command) picks up new content. Restart the agent after. ## Reference - **Repo** - [github.com/indykite/skills](https://github.com/indykite/skills): full `SKILL.md` sources, testing harness, contribution guide. - **Skills CLI** - [skills.sh](https://skills.sh): supported agents, install flags, telemetry policy. - **MCP server** - [indykite-mcp-server/SKILL.md](https://github.com/indykite/skills/blob/main/indykite-mcp-server/SKILL.md): endpoint layout, headers, session lifecycle. - **Agent Gateway** - [indykite-agent-gateway/SKILL.md](https://github.com/indykite/skills/blob/main/indykite-agent-gateway/SKILL.md): deployment shape, policy model. - **Issues, missing skills, security reports** - [responsible_disclosure.md](https://github.com/indykite/skills/blob/main/responsible_disclosure.md) for security; everything else via the [GitHub issues](https://github.com/indykite/skills/issues) tracker. --- Source: https://developer.indykite.com/guides/guide-skills --- # Create Terraform configurations in the IndyKite platform > How to use the IK Terraform plugin to create configurations in the IK platforms. **Category:** Environment ## Summary Getting started with Terraform in the IndyKite platform. ## Content ## What is Terraform? Terraform is an infrastructure-as-code tool that lets you define and manage IndyKite configurations in declarative configuration files. This guide helps you set up Terraform to create configurations in the IndyKite platform. ## How do I install Terraform? ### Mac ```bash brew tap hashicorp/tap brew install hashicorp/tap/terraform terraform --version ``` ### Ubuntu ```bash sudo apt update brew install terraform terraform -v or sudo apt install terraform -y terraform -version ``` ## What credentials do I need? You need **Service Account credentials** to use Terraform with IndyKite. Export them as environment variables: ```bash Credentials: export INDYKITE_SERVICE_ACCOUNT_CREDENTIALS_FILE=lnk-to-the-service-account-credentials Or export INDYKITE_SERVICE_ACCOUNT_CREDENTIALS=content-of-service-account-credentials ``` **Where do I get Service Account credentials?** - Create them in the IndyKite Hub at the Organization level - See [Credentials Guide](/guides/guide-credentials) for details ## How do I structure my Terraform files? Create a `main.tf` file in a directory. Each configuration requires: 1. **terraform block**: Declares the IndyKite provider 2. **provider block**: Configures the IndyKite provider 3. **resource/data blocks**: Defines the resources to create or reference ## How do I create a project environment? This example creates the complete environment hierarchy: Project (ApplicationSpace), Application, Application Agent, and Credentials. ```hcl terraform { required_providers { indykite = { source = "indykite/indykite" version = 1.30.0 # or latest version } } } provider "indykite" {} # call the indykite_customer datasource data "indykite_customer" "customer1" { name = "your-customer-name" } # call the indykite_application_space resource to create a new project resource "indykite_application_space" "appspace1" { customer_id = data.indykite_customer.customer.id name = "project-name" display_name = "Prject display name" description = "Description of your project" region = "us-east1" ikg_size = "4GB" replica_region = "us-west1" } # call the indykite_application_space resource to create a new project with your own DB resource "indykite_application_space" "appspace2" { customer_id = data.indykite_customer.customer.id name = "terraform-pipeline-appspace2" display_name = "Terraform appspace 2" description = "Application space for terraform pipeline" region = "europe-west1" # or us-east1 db_connection { url = "neo4j+s://xxxxxxxx.databases.neo4j.io" username = "testuser" password = "testpass" name = "testdb" } } # call the indykite_application resource to create a new application resource "indykite_application" "application1" { app_space_id = indykite_application_space.appspace.id name = "application-name" display_name = "Application display name" description = "Description of your application" } # call the indykite_application_agent to create a new application agent resource "indykite_application_agent" "agent" { application_id = indykite_application.application.id name = "application-agent-name" display_name = "Application agent display name" description = "Description of your application agent" } # call the indykite_application_agent_credential to create a new application agent credential resource "indykite_application_agent_credential" "with_public" { app_agent_id = indykite_application_agent.agent.id display_name = "Credential display name" expire_time = "2026-12-31T12:34:56-01:00" #must be less than 2 years to generate a token } ``` ### What resources are created? | Resource | Description | | -------- | ----------- | | `indykite_application_space` | Project container with its own IKG. Use `ikg_size` for managed IKG or `db_connection` for your own Neo4j. | | `indykite_application` | Application within the project | | `indykite_application_agent` | Agent identity for API authentication | | `indykite_application_agent_credential` | Token for the agent (expires in max 2 years) | ## How do I create a KBAC policy? KBAC policies define authorization rules. This example creates a policy that allows a Person to drive a Car they own. ```hcl terraform { required_providers { indykite = { source = "indykite/indykite" version = 1.30.0 # or latest version } } } provider "indykite" {} resource "indykite_authorization_policy" "policy_drive_car" { name = "terraform-pipeline-policy-drive-car" display_name = "Terraform policy drive car" description = "Policy for terraform pipeline" json = jsonencode({ meta = { policyVersion = "1.0-indykite" }, subject = { type = "Person" }, actions = ["CAN_DRIVE"], resource = { type = "Car" }, condition = { cypher = "MATCH (subject:Person)-[:OWNS]->(resource:Car)" } }) location = indykite_application_space.appspace.id status = "active" } ``` ### What are the key policy fields? | Field | Description | | ----- | ----------- | | `meta.policyVersion` | Use `1.0-indykite` for KBAC policies | | `subject.type` | Node type of the requesting entity (e.g., `Person`) | | `actions` | Array of action names (e.g., `CAN_DRIVE`, `CAN_READ`) | | `resource.type` | Node type of the resource being accessed (e.g., `Car`) | | `condition.cypher` | Graph pattern that must exist for access to be granted | ## How do I create a CIQ policy and Knowledge Query? CIQ (ContX IQ) policies use `1.0-ciq` version and support read/write operations. This example uses `_Application` as the subject. ```hcl terraform { required_providers { indykite = { source = "indykite/indykite" version = 1.30.0 # or latest version } } } provider "indykite" {} resource "indykite_authorization_policy" "policy_for_ciq" { name = "terraform-pipeline-policy-for-ciq" display_name = "Terraform policy for CIQ" description = "Policy for CIQ in terraform pipeline" json = jsonencode({ "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "_Application" }, "condition": { "cypher": "MATCH (subject:_Application) MATCH (person:Person)-[r1:ACCEPTED]->(contract:Contract)-[r2:COVERS]->(vehicle:Vehicle)-[r3:HAS]->(ln:LicenseNumber)", "filter": [ { "operator": "AND", "operands": [ { "attribute": "person.property.email", "operator": "=", "value": "$person_email" }, { "attribute": "subject.external_id", "operator": "=", "value": "$_appId" } ] } ] }, "allowed_reads":{ "nodes":["ln.property.number", "ln.property.transferrable"], "relationships":[] } }) location = indykite_application_space.appspace.id status = "active" } resource "indykite_knowledge_query" "create-query" { name = "terraform-knowledge-query" display_name = "Terraform knowledge-query" description = "Knowledge query for terraform" location = indykite_application_space.appspace.id query = jsonencode({ "nodes" : ["ln.property.number"], "relationships" : [], "filter" : { "attribute" : "ln.property.number", "operator" : "=", "value" : "$ln_number" } }) status = "active" policy_id = indykite_authorization_policy.policy_for_ciq.id } ``` ### What is special about _Application as subject? When using `_Application` as the subject type: - The `$_appId` parameter is automatically populated with the Application's external_id - No user access token is required for CIQ execution - Useful for service-to-service authorization ### What does the Knowledge Query define? | Field | Description | | ----- | ----------- | | `nodes` | Node variables to return in results | | `relationships` | Relationship variables to return in results | | `filter` | Additional filters with partial parameters (prefixed with `$`) | | `policy_id` | Links the query to its authorization policy | ## How do I execute Terraform? ### Step 1: Initialize the working directory Downloads the IndyKite provider and sets up the working directory. ```bash terraform init ``` ### Step 2: Preview the changes Shows what Terraform will create, modify, or destroy without making changes. ```bash terraform plan ``` ### Step 3: Apply the configuration Creates or updates the resources in IndyKite. ```bash terraform apply ``` ### How do I destroy resources? To remove all resources defined in your configuration: ```bash terraform destroy ``` ## Next Steps - Terraform provider documentation: [IndyKite Terraform Provider](https://registry.terraform.io/providers/indykite/indykite/latest/docs) - External Data Resolver: [Configure external API lookups](/guides/guide-external-data-resolver) - Trust Score Profile: [Assess data quality](/guides/guide-trust-score) - More Terraform examples: [Terraform Configurations](/terraform) - Full examples: [Developer Hub Resources](/resources) - Credentials guide: [Credentials Guide](/guides/guide-credentials) --- Source: https://developer.indykite.com/guides/guide-terraform --- # Token Introspect: Validate External Identity Tokens > What is needed to configure Token Introspect for the IndyKite platform. **Category:** Token Introspect ## Summary What are the key components and steps for creating a token introspection configuration? ## Content # What is Token Introspection? Token introspection is the process through which a **resource server** validates an access or refresh token by querying the **authorization server**, typically via an introspection endpoint. This yields **metadata (claims)** about the token—such as subject (`sub`), scopes, issuer, and expiration. Standardized by **OAuth 2.0 Token Introspection (RFC 7662)**, introspection is critical for validating **opaque tokens** and can also play a role in managing **JWTs** across distributed systems. # What token types does IndyKite support? ## Opaque Tokens **What are opaque tokens?** - Random strings with no readable structure. - Must be validated **online** via `/introspect` or `/userinfo` endpoints. - Commonly issued by providers like **Okta**, **Auth0**, **Keycloak**. - Enable **centralized revocation** and token lifecycle management. **How does opaque token validation work?** - **Client** sends the token to a **resource server**. The resource server sends a **POST request** to the **introspection endpoint**: POST /introspect Authorization: Basic base64(client_id:client_secret) Content-Type: application/x-www-form-urlencoded token=ACCESS_TOKEN The **authorization server** returns JSON describing the token: { "active": true, "scope": "read write", "client_id": "client123", "username": "alice", "sub": "user123", ... } - The **resource server** uses the returned claims for **authorization** and **access control** decisions. ## JWT Tokens (JSON Web Tokens) **What are JWT tokens?** - Self-contained, signed tokens that include structured claims. Can be **validated offline**, without contacting the issuer, by: - Decoding the payload. - Verifying the **signature** using a public key (via JWKS or `.well-known`). Use introspection optionally for: - **Claim enrichment**. - **Additional validation** logic or identity mapping. **How does JWT validation work?** - **Receives** the JWT (e.g., via `Authorization: Bearer ...` header). - **Decodes** the token (Base64) to inspect claims. **Verifies** the signature using: - A **public key** (RS256) from the issuer's JWKS endpoint. - Or a **shared secret** (HS256). - Uses verified claims (`sub`, `exp`, `scope`, `aud`, etc.) to enforce access policies. Introspection endpoints are not necessary for JWTs when the token can be fully validated offline. # How does IndyKite handle third-party tokens? IndyKite APIs can identify users via **third-party tokens** through **token introspection configuration**. This configuration enables the IndyKite platform to validate these tokens and utilize their content. ## Step 1: How do I configure token validation? The configuration specifies how to validate the token. You can choose between: ### JWT (Offline Validation) Validate using public keys or `.well-known/jwks.json`. **Terraform example:** resource "indykite_token_introspect" "token_config" { name = "terraform-token-introspect" display_name = "Terraform token introspect" description = "Token introspect for User access token" location = "ProjectGID" jwt_matcher { -> JWT specifies all attributes to match with received token. issuer = "https://xx.xx.auth0.com/" -> Issuer is used to exact match based on iss claim in JWT. audience = "client-id" -> Audience is used to exact match based on aud claim in JWT } offline_validation {} -> Offline validation works only with JWT. If public_jwks is empty, will be generated ikg_node_type = "Person" -> Node type in IKG to which we will try to match sub claim with DT external_id. claims_mapping = { -> ClaimsMapping specifies which claims from the token should be mapped to IKG Property with given name. "email" = "email" "phone_number" = "phone_number" } perform_upsert = true -> Perform Upsert specify, if we should create and/or update Identity node in IKG if it doesn't exist with. } ### Opaque (Online Validation) Use `/userinfo` or introspection endpoints. **Terraform example:** resource "indykite_token_introspect" "token_config" { name = "terraform-token-introspect" display_name = "Terraform token introspect" description = "Token introspect for User access token" location = "ProjectGID" opaque_matcher { -> Opaque specifies the configuration is for opaque tokens. hint = "my.domain.com" } online_validation { user_info_endpoint = "https://example.com/userinfo" -> URI of userinfo endpoint which will be used to validate access token and also fetch user claims when opaque token is received. It can remain empty, if JWT token matcher is used cache_ttl = 600 -> Cache TTL of token validity can be used to minimize calls to userinfo endpoint. If not set, token will not be cached and call to userinfo endpoint will be made on every request } ikg_node_type = "Person" -> Node type in IKG to which we will try to match sub claim with identity node external_id. claims_mapping = { -> ClaimsMapping specifies which claims from the token should be mapped to IKG Property with given name. "email" = "email" "phone_number" = "phone_number" } perform_upsert = true -> Perform Upsert specify, if we should create and/or update Identity node in IKG if it doesn't exist with. } ### What do the configuration fields mean? Field Description `jwt_matcher.issuer` Exact match for the `iss` claim in the JWT `jwt_matcher.audience` Exact match for the `aud` claim in the JWT `opaque_matcher.hint` Domain hint for opaque token identification `offline_validation` Use for JWT tokens; validates using public keys `online_validation.user_info_endpoint` Endpoint to validate opaque tokens and fetch claims `online_validation.cache_ttl` Seconds to cache token validity (reduces endpoint calls) `ikg_node_type` Node type in IKG to match with token subject (e.g., `Person`) `claims_mapping` Map token claims to IKG property names `perform_upsert` If true, creates/updates identity node in IKG when not found ## Step 2: How does subject mapping work? After validating the token, IndyKite matches the token subject to an identity node in the IKG. **What information is needed for matching?** **Subject claim**: Usually the `sub` claim from the third-party IDP, which equals `external_id` in the IKG. You can also use a different claim to identify the subject. **Important:** Never use `external_id` as a `claims_mapping` key—it's used for matching. - **Node type**: Specified in the configuration via `ikg_node_type`. Required because `external_id` is only unique per node type, not across the entire IKG. ## Step 3: How does claims mapping work? The `claims_mapping` attribute specifies which claims from the token should be mapped to IKG property names. **Key**: The new claim name and IKG property name. - Max length: 256 characters - Pattern: `^[a-zA-Z_][a-zA-Z0-9_]+$` - **Value**: The token claim to map from. **Which claims are supported?** All standard claims from the OpenID specification are supported. Mapping will fail if claim and data type do not match the standard. For non-standard claims, the type is derived from the JSON. **Warning:** Claims mapping can override existing claims, which may affect internal services. ## Step 4: What is Just-In-Time (JIT) provisioning? When `perform_upsert = true`, IndyKite supports **Just-In-Time provisioning**: automatically creating or updating an identity node in the IKG based on token claims. **How does JIT provisioning work?** - When a user accesses the system for the first time, their identity node is created on-demand using token attributes. - The user is granted access immediately based on their new local identity. **Important:** For performance reasons, if a created identity node is deleted, token introspection will not create a new node with the same token. A new token must be generated. # What configuration options are available? Option JWT Opaque **Issuer** Required Not used **Audience (client_id)** Required Not used **Public Key (JWKS)** Used for signature verification Not used **User Info / Introspect Endpoint** Optional for Offline / Required for Online Required **Caching** JWKS & claims caching Token metadata caching **Validation Method** Offline or Online Online only # What claims are commonly used? Claim Description `sub` Subject (user ID) of the token `username` Login name of the user `email` Email of the user `client_id` OAuth client that obtained the token `exp`, `iat` Expiration and issued-at timestamps `scope` Space-separated list of access scopes `aud` Intended audience(s) `iss` Issuer of the token `jti` Unique token identifier `roles`, `permissions` Optional claims for access control `acr` Authentication Context Class Reference (used for step-up authentication) # What features does IndyKite Token Introspect support? Feature Supported JWT introspection Yes Opaque token introspection Yes (Online) Third-party token exchange Yes Public key & metadata caching Yes Multi-IDP support Yes Claims mapping Yes (Configurable) Just-In-Time provisioning (node upsert into IKG) Yes (Optional) # Next Steps - Terraform examples: Token Introspect Terraform Configuration - REST API: Config API Documentation - Full examples: Developer Hub Resources - Credentials guide: Credentials Guide --- Source: https://developer.indykite.com/guides/guide-token-introspect --- # Trust Score: Assess Data Trustworthiness > How to configure Trust Score Profiles to assess data trustworthiness based on freshness, origin, validity, completeness, and verification. **Category:** Trust Score ## Summary How to create Trust Score Profiles, ingest nodes with metadata, and use trust scores in authorization decisions. ## Content # What is Trust Score? Trust Score is IndyKite's data quality assessment system. It evaluates **how trustworthy your data is** based on configurable dimensions like freshness, origin, and verification status. Key benefits: - **Data quality visibility**: Know which data is fresh, verified, and complete. - **Risk-based decisions**: Use trust scores in authorization policies (KBAC) and queries (ContX IQ). - **Automated recalculation**: Scores update on a schedule as data ages or changes. - **Flexible weighting**: Prioritize the dimensions that matter for your use case. # How does it work? Trust Score involves three components: - **Trust Score Profile**: Configuration defining which dimensions to evaluate and their weights. - **Node metadata**: Properties on nodes that provide input for scoring (e.g., `source`, `verified_time`). - **_TrustScore node**: Automatically created in the IKG, linked to the scored node via `_HAS` relationship. ## Scoring flow - Create a Trust Score Profile specifying dimensions and weights - Ingest nodes with metadata (source, verified_time, etc.) - IndyKite calculates scores based on the configured schedule - A `_TrustScore` node is created/updated for each scored node - Query trust scores via ContX IQ or use in KBAC policies # What credentials do I need? - **Creating profiles**: Service Account credentials (Config API) - **Ingesting nodes with metadata**: AppAgent credentials (Capture API) - **Querying trust scores**: AppAgent credentials + optional user access token (ContX IQ) Configuration methods: - Terraform: indykite_trust_score_profile resource - REST API: Config API documentation # Trust Score Dimensions Five dimensions can be used to evaluate data quality: **Dimension** **Description** **Input metadata** `FRESHNESS` How recent is the data? Older data scores lower. Property update timestamps `ORIGIN` Where did the data come from? Trusted sources score higher. `source` metadata on properties `VALIDITY` Does the data comply with expected formats and rules? Format validation results `COMPLETENESS` Are all critical fields present? Presence of required properties `VERIFICATION` Has the data been verified/confirmed? `verified_time` metadata Each dimension has a `weight` (0-1). The weighted combination produces the overall trust score. # Schedule Options Trust scores are recalculated periodically based on the schedule: **Schedule Value** **Description** `THREE_HOURS` Recalculate every 3 hours `SIX_HOURS` Recalculate every 6 hours `TWELVE_HOURS` Recalculate every 12 hours `DAILY` Recalculate once per day # Trust Score Profile Configuration ## REST API Endpoints **Operation** **Method** **Endpoint** Create POST `/configs/v1/trust-score-profiles` Read by ID GET `/configs/v1/trust-score-profiles/{id}` Read by name GET `/configs/v1/trust-score-profiles/{name}?location={project_id}` List all GET `/configs/v1/trust-score-profiles?project_id={id}` Update PUT `/configs/v1/trust-score-profiles/{id}` Delete DELETE `/configs/v1/trust-score-profiles/{id}` ## Create Request Syntax { "project_id": "", "name": "", "display_name": "", "description": "", "node_classification": "", "schedule": "", "dimensions": [ { "name": "", "weight": } ] } ### What does each field mean? Required fields - `project_id`: The GID of the project where the profile will be created. - `name`: Unique, immutable identifier. Must start with a lowercase letter, contain only lowercase letters, numbers, and hyphens. - `node_classification`: The node type (label) to score. Must be PascalCase (e.g., `Person`, `Organization`, `Asset`). - `schedule`: How often to recalculate scores. See schedule options above. - `dimensions`: Array of at least one dimension with name and weight. Optional fields - `display_name`: Human-readable name (can be updated). - `description`: Description of the profile (max 65000 UTF-8 bytes). Dimension object - `name`: One of `FRESHNESS`, `ORIGIN`, `VALIDITY`, `COMPLETENESS`, `VERIFICATION`. - `weight`: A number between 0 and 1 indicating the importance of this dimension. ## Example: Create Trust Score Profile POST /configs/v1/trust-score-profiles { "project_id": "gid:AAAABbbbCCCC...", "name": "person-trust-profile", "display_name": "Person Trust Score", "description": "Evaluates trustworthiness of Person nodes", "node_classification": "Person", "schedule": "TWELVE_HOURS", "dimensions": [ { "name": "FRESHNESS", "weight": 0.3 }, { "name": "ORIGIN", "weight": 0.4 }, { "name": "VERIFICATION", "weight": 0.3 } ] } ## Example: Update Trust Score Profile PUT /configs/v1/trust-score-profiles/{id} { "display_name": "Updated Person Trust Score", "schedule": "THREE_HOURS", "dimensions": [ { "name": "FRESHNESS", "weight": 1 }, { "name": "ORIGIN", "weight": 1 } ] } ## Read Response Reading a profile (`GET /configs/v1/trust-score-profiles/{id}`) returns its configuration plus execution metadata, including the last-run details. (On the **List** endpoint, pass `full_fetch=true` to get full objects instead of just metadata.) { "id": "gid:AAAABbbbCCCC...", "name": "person-trust-profile", "display_name": "Person Trust Score", "description": "Evaluates trustworthiness of Person nodes", "node_classification": "Person", "schedule": "TWELVE_HOURS", "dimensions": [ { "name": "FRESHNESS", "weight": 0.3 } ], "organization_id": "gid:...", "project_id": "gid:...", "create_time": "2024-01-15T10:30:00Z", "update_time": "2024-01-15T10:30:00Z", "created_by": "gid:...", "updated_by": "gid:...", "last_run_id": "gid:...", "last_run_start_time": "2024-01-15T12:00:00Z", "last_run_end_time": "2024-01-15T12:00:05Z", "dimensions_execution_times": {...} } # Ingesting Nodes with Metadata For trust scoring to work, nodes must have metadata on their properties. Use the Capture API to ingest nodes with metadata. ## Metadata fields for Trust Score **Metadata field** **Used by dimension** **Description** `source` ORIGIN Where the data came from (e.g., "passport", "HR_System") `verified_time` VERIFICATION, FRESHNESS When the data was last verified (ISO 8601 timestamp) `assurance_level` VERIFICATION Confidence level (numeric) ## Capture API Request with Metadata POST /capture/v1/nodes { "nodes": [ { "external_id": "jane-doe", "type": "Person", "is_identity": true, "properties": [ { "type": "name", "value": "Jane Doe", "metadata": { "source": "passport", "verified_time": "2024-01-10T14:30:00Z" } }, { "type": "email", "value": "jane@example.com", "metadata": { "source": "self_reported", "verified_time": "2024-01-08T09:00:00Z" } }, { "type": "passport_id", "value": "A67897XYZ", "metadata": { "source": "passport", "assurance_level": 3, "verified_time": "2024-01-10T14:30:00Z" } } ] } ] } Properties with `source: "passport"` and recent `verified_time` will score higher than self-reported data with older timestamps. # Trust Score in the IKG After the trust score profile runs, a `_TrustScore` node is created and linked to each scored node: `(Person:jane-doe)-[:_HAS]->(_TrustScore)` The `_TrustScore` node contains: - Overall trust score value - Individual dimension scores - Calculation timestamp # Querying Trust Scores with ContX IQ Use ContX IQ to query trust scores and use them in authorization decisions. **Important:** `_TrustScore` is an internal node label that CIQ cypher is not allowed to match directly. Instead, read the score through the virtual `trust_score` accessor on the scored node: `.trust_score._final_score` for the overall score, or `.trust_score.` / `.trust_score.*` for individual dimensions. ## CIQ Policy including Trust Score { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "Person" }, "condition": { "cypher": "MATCH (subject:Person)" }, "allowed_reads": { "nodes": [ "subject.property.*", "subject.trust_score._final_score", "subject.trust_score.*" ] } } ## Knowledge Query for Trust Score { "nodes": [ "subject.property.name", "subject.property.email", "subject.trust_score._final_score" ] } # Using Trust Score in KBAC Trust scores can be used in KBAC policies to make authorization decisions based on data quality. As in CIQ, do not match the `_TrustScore` node in cypher - read the score through the `subject.trust_score._final_score` accessor in the condition filter: { "meta": { "policy_version": "2.0-kbac" }, "subject": { "type": "Person" }, "actions": ["CAN_ACCESS"], "resource": { "type": "Document" }, "condition": { "cypher": "MATCH (subject:Person) MATCH (resource:Document)", "filter": { "operator": ">", "attribute": "subject.trust_score._final_score", "value": 0.8 } } } This policy only allows access if the person's `_final_score` exceeds 0.8. The same policy is also valid with `"policy_version": "3.0-kbac"` - the raw-Cypher version used for data-residency routing (see the data residency guide). # Terraform Configuration Use the `indykite_trust_score_profile` resource: resource "indykite_trust_score_profile" "person_trust" { location = indykite_application_space.my_app_space.id name = "person-trust-profile" display_name = "Person Trust Score" description = "Evaluates trustworthiness of Person nodes" node_classification = "Person" schedule = "UPDATE_FREQUENCY_TWELVE_HOURS" dimension { name = "NAME_FRESHNESS" weight = 0.3 } dimension { name = "NAME_ORIGIN" weight = 0.4 } dimension { name = "NAME_VERIFICATION" weight = 0.3 } } ### Terraform Arguments Reference **Argument** **Required** **Description** `location` Yes Application Space ID where the profile is created `name` Yes Unique, immutable identifier `node_classification` Yes Node type to score (PascalCase) `schedule` Yes Recalculation frequency `dimension` Yes At least one dimension block `display_name` No Human-readable name `description` No Profile description ### Terraform Schedule Values - `UPDATE_FREQUENCY_THREE_HOURS` - `UPDATE_FREQUENCY_SIX_HOURS` - `UPDATE_FREQUENCY_TWELVE_HOURS` - `UPDATE_FREQUENCY_DAILY` ### Terraform Dimension Names - `NAME_FRESHNESS` - `NAME_ORIGIN` - `NAME_VALIDITY` - `NAME_COMPLETENESS` - `NAME_VERIFICATION` # Error Handling **HTTP Code** **Meaning** **Common Cause** 400 Bad Request Invalid JSON, missing required fields 401 Unauthorized Invalid or missing Bearer token 403 Forbidden Insufficient permissions for the project 404 Not Found Profile ID/name doesn't exist 412 Precondition Failed ETag mismatch (concurrent modification) 422 Unprocessable Entity Validation error (invalid name, missing dimensions, etc.) ### Common validation errors - `invalid field name: is not valid name` - Name must start with lowercase letter, contain only lowercase letters, numbers, and hyphens. - `invalid field project_id: identifier is not of PROJECT` - Must use a valid project GID. - `missing field node_classification` - Node type is required. - `missing field schedule` - Schedule is required. - `missing field dimensions` - At least one dimension is required. # Best Practices ### Dimension weighting - Weight dimensions based on your use case requirements. - For identity verification: prioritize `VERIFICATION` and `ORIGIN`. - For real-time data: prioritize `FRESHNESS`. - For compliance: prioritize `COMPLETENESS` and `VALIDITY`. ### Schedule selection - Use `THREE_HOURS` for data that changes frequently and freshness is critical. - Use `DAILY` for stable data where frequent recalculation adds overhead. - `TWELVE_HOURS` is a good default for most use cases. ### Metadata ingestion - Always include `source` metadata to enable ORIGIN scoring. - Update `verified_time` when data is re-verified. - Use consistent source names across your data pipeline. # Next Steps - ContX IQ guide: ContX IQ Guide - KBAC guide: Dynamic Authorization Guide - Terraform provider: Trust Score Profile Resource - Credentials guide: Credentials Guide --- Source: https://developer.indykite.com/guides/guide-trust-score --- # Audit Signing: Manage Signing Key Configurations via REST > Full lifecycle of an Audit Signing configuration through the Config API: create a platform-managed config, create one backed by your own AWS KMS key, read it back with masked auth_params, update it with ETag concurrency control to rotate the key, and delete it. **Category:** Audit Signing **API:** Audit Signing **Tags:** Audit Signing, Config API, KMS, Key Management, BYOK, ETag, Audit Records **Last Updated:** 2026-09-03 **OpenAPI Endpoints:** /configs/v1/audit-signings, /configs/v1/audit-signings/{id} **Related Guides:** /guides/guide-audit-signing, /guides/guide-credentials, /guides/guide-environment ## Summary This example demonstrates the Audit Signing configuration REST endpoints (/configs/v1/audit-signings). What is an Audit Signing configuration? It declares who manages the cryptographic key used to sign your project's audit records, making them tamper-evident: - PLATFORM_MANAGED (default): IndyKite manages the signing key - CUSTOMER_GCP_KMS / CUSTOMER_AWS_KMS / CUSTOMER_AZURE_KEY_VAULT: signing uses your own key in your cloud key store - identified by key_resource (e.g. the AWS KMS key ARN, max 256 chars) and kid (the key ID stamped on signatures, max 256 chars), with provider access material in auth_params (max 32 string pairs) Key behaviors shown: 1. Create with defaults - name, project_id and provider. 2. Create with a customer-managed key - key_resource and kid become required 3. Read - by GID or by name + project_id/location; auth_params values come back masked as empty strings 4. Update with If-Match ETag - rotate to a new key by changing key_resource and kid 5. Delete with the etag query parameter - 204 No Content Validation errors are explicit: a 422 response lists the exact reason, e.g. "key_resource is required for customer-managed providers". ## Use Case Scenario: A compliance team must prove that audit records have not been altered, using a signing key the company controls. Flow: 1. The team creates an AWS KMS key and an IAM role that permits signing with it 2. Ops creates an Audit Signing configuration: provider CUSTOMER_AWS_KMS, key_resource = the key's ARN, kid = "audit-2026-q3", auth_params.role_arn = the role 3. The project's audit records are now signed with the customer-held key; verifiers use the kid to select the right public key 4. At the quarterly rotation, ops PUTs the configuration with the new key ARN and kid "audit-2026-q4", guarded by If-Match so concurrent edits fail loudly (412) 5. Reading the configuration never leaks secrets: auth_params values are masked Key custody stays with the company: revoking the key or the role in AWS immediately withdraws the platform's ability to sign with it. ## Requirements Prerequisites: - ServiceAccount credentials: All /configs/v1/audit-signings endpoints authenticate with a Service Account Bearer token - Project GID: The project (application space) the configuration belongs to - For customer-managed providers: a signing key in GCP KMS, AWS KMS, or Azure Key Vault, plus the access material the platform should use (e.g. an AWS IAM role ARN) Field constraints: - name: URL-friendly, unique in the project, immutable - display_name: 2-254 chars (optional) - description: 2-65000 chars (optional) - provider: PLATFORM_MANAGED | CUSTOMER_GCP_KMS | CUSTOMER_AWS_KMS | CUSTOMER_AZURE_KEY_VAULT - key_resource, kid: max 256 chars; required for every customer-managed provider - auth_params: up to 32 string pairs; write-only (masked on read) ## Steps Step 1: Create a platform-managed configuration - POST /configs/v1/audit-signings with name, project_id, provider PLATFORM_MANAGED - Result: 201 with id, timestamps, authorship - and an ETag response header Step 2: Create a customer-managed configuration (AWS KMS) - POST with provider CUSTOMER_AWS_KMS, key_resource (key ARN), kid, auth_params.role_arn - Omitting key_resource or kid returns 422 with the exact validation error Step 3: Read the configuration - GET /configs/v1/audit-signings/{id}, or /{name}location=; optional version query parameter - auth_params values are masked as empty strings; response carries a fresh ETag Step 4: List the project's configurations - GET /configs/v1/audit-signings?project_id= returns { "data": [...] } Step 5: Update to rotate the key - PUT /configs/v1/audit-signings/{id} with If-Match: ; provider is required on update - New key_resource + new kid = key rotation; a stale ETag returns 412 Precondition Failed - For display_name/description: null keeps the value, empty string removes it Step 6: Delete - DELETE /configs/v1/audit-signings/{id}?etag= returns 204 No Content ## Code Examples ### Step 1 Create a platform-managed Audit Signing configuration - the minimal form. Choose PLATFORM_MANAGED provider. **POST https://eu.api.indykite.com/configs/v1/audit-signings** ```json { "name": "audit-signing-default", "project_id": "gid-of-project", "provider": "PLATFORM_MANAGED" } ``` ### Step 2 Create a configuration backed by your own AWS KMS key. key_resource identifies the key (its ARN), kid is the key ID stamped on signatures, and auth_params carries the access material (here an IAM role ARN). For GCP KMS or Azure Key Vault, use that provider's key identifier in key_resource. **POST https://eu.api.indykite.com/configs/v1/audit-signings** ```json { "name": "audit-signing-byok", "display_name": "Audit signing with our AWS KMS key", "description": "Signs audit records with the compliance team key", "project_id": "gid-of-project", "provider": "CUSTOMER_AWS_KMS", "key_resource": "arn:aws:kms:us-east-1:123456789012:key/example-key-id", "kid": "audit-2026-q3", "auth_params": { "role_arn": "arn:aws:iam::123456789012:role/indykite-audit-signer" } } ``` Create response (201): the configuration GID, timestamps, and authorship. Save the ETag response header for Step 5 and Step 6. ```json { "id": "gid:example-audit-signing-config-id", "create_time": "2026-09-03T09:00:00Z", "created_by": "gid:example-service-account-id", "update_time": "2026-09-03T09:00:00Z", "updated_by": "gid:example-service-account-id" } ``` ### Step 3 Read the configuration by GID, or by name with the project_id query parameter. **GET https://eu.api.indykite.com/configs/v1/audit-signings/{id}** ```bash curl /configs/v1/audit-signings/?project_id= \ -H "Authorization: Bearer $SERVICE_ACCOUNT_TOKEN" ``` Read response: all fields plus organization_id - with auth_params values masked as empty strings. Secrets never round-trip. ```json { "id": "gid:example-audit-signing-config-id", "name": "audit-signing-byok", "display_name": "Audit signing with our AWS KMS key", "description": "Signs audit records with the compliance team key", "create_time": "2026-09-03T09:00:00Z", "created_by": "gid:example-service-account-id", "update_time": "2026-09-03T09:00:00Z", "updated_by": "gid:example-service-account-id", "organization_id": "gid:example-organization-id", "project_id": "gid-of-project", "provider": "CUSTOMER_AWS_KMS", "key_resource": "arn:aws:kms:us-east-1:123456789012:key/example-key-id", "kid": "audit-2026-q3", "auth_params": { "role_arn": "" } } ``` ### Step 4 List every Audit Signing configuration in the project. **GET https://eu.api.indykite.com/configs/v1/audit-signings?project_id=** ```bash curl /configs/v1/audit-signings?project_id= \ -H "Authorization: Bearer $SERVICE_ACCOUNT_TOKEN" # Response: { "data": [ ] } ``` ### Step 5 Rotate the key: PUT with If-Match carrying the last ETag, pointing key_resource at the new key and bumping kid so verifiers can tell old signatures from new. provider is required on update. **PUT https://eu.api.indykite.com/configs/v1/audit-signings/{id}** ```bash curl -X PUT /configs/v1/audit-signings/ \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $SERVICE_ACCOUNT_TOKEN" \ -H "If-Match: $ETAG" \ -d '{ "provider": "CUSTOMER_AWS_KMS", "key_resource": "arn:aws:kms:us-east-1:123456789012:key/rotated-key-id", "kid": "audit-2026-q4", "auth_params": { "role_arn": "arn:aws:iam::123456789012:role/indykite-audit-signer" } }' ``` ### Step 6 Delete the configuration, guarded by the etag query parameter. **DELETE https://eu.api.indykite.com/configs/v1/audit-signings/{id}** ```bash curl -X DELETE "/configs/v1/audit-signings/?etag=$ETAG" \ -H "Authorization: Bearer $SERVICE_ACCOUNT_TOKEN" # Response: 204 No Content ``` Validation error contract: creating a customer-managed configuration without its key fields returns 422 with the exact reason in the errors array. ```json { "message": "Unprocessable Entity", "errors": [ "key_resource is required for customer-managed providers" ] } ``` ## Common Errors ### 422: key_resource is required for customer-managed providers / kid is required for customer-managed providers / provider is required **Solution:** Every customer-managed provider (CUSTOMER_GCP_KMS, CUSTOMER_AWS_KMS, CUSTOMER_AZURE_KEY_VAULT) needs both key_resource and kid; provider itself is required on update. ### 412: Precondition Failed **Solution:** The If-Match ETag (or etag query parameter on delete) no longer matches the stored configuration - re-read to get the current ETag and retry. ### 409: Conflict **Solution:** A configuration with the same name already exists in the project - name is unique per project and immutable. ### 404: Not Found **Solution:** No configuration with that GID or name in the project (reads by name need the project_id query parameter); deleted configurations also return 404. --- Source: https://developer.indykite.com/resources/audit-signing-1 --- # KBAC: Relationship-Based Authorization with authZEN API > Create a Knowledge-Based Access Control (KBAC) policy and execute authZEN-compliant authorization queries. This example demonstrates evaluation (can X do Y to Z?) and search operations (who can? what can? which resources?). **Category:** KBAC **API:** KBAC **Tags:** KBAC Policy, authZEN, Authorization, Evaluation, Action Search, Resource Search, Subject Search, Access Control **Last Updated:** 2026-07-14 **OpenAPI Endpoints:** /capture/v1/nodes, /capture/v1/relationships, /configs/v1/authorization-policies, /access/v1/evaluation, /access/v1/search/action, /access/v1/search/resource, /access/v1/search/subject **Related Guides:** /guides/guide-authzen, /guides/guide-dynamic-authz, /guides/guide-sandbox ## Summary This example demonstrates KBAC (Knowledge-Based Access Control) using the authZEN standard API: Policy logic: A Person can DRIVE a Car if they have a DRIVES relationship to that Car in the graph. authZEN operations demonstrated: 1. Evaluation: "Can Alice drive Car1?" -> true/false 2. Action Search: "What actions can Alice perform on Car1?" -> ["drive"] 3. Resource Search: "Which cars can Alice drive?" -> ["Car1", "Car2"] 4. Subject Search: "Who can drive Car1?" -> ["Alice", "Bob"] authZEN is an OpenID standard for authorization APIs (https://openid.net/specs/authorization-api-1_0-03.html). Policy version note: the policy in this example uses "policy_version": "2.0-kbac". The same policy JSON is also accepted with "policy_version": "3.0-kbac" (identical schema, raw-Cypher semantics; do not reference $subject_id or external properties). Only 3.0-kbac additionally accepts USE graph.byName() routing and CALL { } subqueries for composite / data-residency IKGs - see resources authz-7 and authz-8. ## Use Case Scenario: A car-sharing application needs to verify driving permissions. Graph structure: - Person(Alice) -[DRIVES]-> Car(Car1) - Person(Alice) -[DRIVES]-> Car(Car2) - Person(Bob) -[DRIVES]-> Car(Car1) Authorization questions the app can ask: 1. "Can Alice drive Car1?" -> Yes (DRIVES relationship exists) 2. "Can Alice drive Car3?" -> No (no DRIVES relationship) 3. "What can Alice do with Car1?" -> ["drive"] 4. "Which cars can Alice drive?" -> ["Car1", "Car2"] 5. "Who can drive Car1?" -> ["Alice", "Bob"] Use this pattern for: vehicle access, device permissions, resource sharing, role-based access. ## Requirements Prerequisites: - ServiceAccount credentials: For creating policies (Bearer token) - AppAgent credentials: For data ingestion and authorization queries (X-IK-ClientKey) Required API access: - POST /capture/v1/nodes/ and /capture/v1/relationships/ (graph data) - POST /configs/v1/authorization-policies (create policy) - POST /access/v1/evaluation (single authorization check) - POST /access/v1/search/action (what actions are permitted?) - POST /access/v1/search/resource (which resources can be accessed?) - POST /access/v1/search/subject (who can access this resource?) ## Steps Step 1: Ingest Authorization Graph Data - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: POST Person and Car nodes, DRIVES relationships - Result: Graph ready for authorization queries Step 2: Create KBAC Policy - Authentication: ServiceAccount credential (Bearer token) - Action: POST policy defining: Person can DRIVE Car if DRIVES relationship exists - Result: Policy ID returned Step 3: Run authZEN Evaluation - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: POST to /access/v1/evaluation with subject, action, resource - Input: {subject: "Alice", action: "drive", resource: "Car1"} - Result: {decision: true} or {decision: false} Step 4: Run authZEN Searches - Authentication: AppAgent credential (X-IK-ClientKey header) - Action Search: "What can Alice do with Car1?" -> ["drive"] - Resource Search: "Which cars can Alice drive?" -> ["Car1", "Car2"] - Subject Search: "Who can drive Car1?" -> ["Alice", "Bob"] Step 5: Cleanup - Action: DELETE policy configuration ## Code Examples ### Step 1 Capture the nodes needed for this use case. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "alice", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "alice@email.com" }, { "type": "given_name", "value": "Alice" }, { "type": "last_name", "value": "Smith" } ] }, { "external_id": "knightrider", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "knightrider@demo.com" }, { "type": "name", "value": "Michael Knight" } ] }, { "external_id": "satchmo", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "satchmo@demo.com" }, { "type": "name", "value": "Louis Armstrong" } ] }, { "external_id": "karel", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "karel@demo.com" }, { "type": "name", "value": "Karel Plihal" } ] }, { "external_id": "kitt", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "pontiac" }, { "type": "model", "value": "Firebird" } ] }, { "external_id": "cadillacv16", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "Cadillac" }, { "type": "model", "value": "V-16" } ] }, { "external_id": "harmonika", "type": "Bus", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "Ikarus" }, { "type": "model", "value": "280" } ] }, { "external_id": "listek", "type": "Ticket", "is_identity": false }, { "external_id": "airbook-xyz", "type": "Laptop", "is_identity": false } ] } ``` Capture the relationships needed for this use case. **POST https://eu.api.indykite.com/capture/v1/relationships/** ```json { "relationships": [ { "source": { "external_id": "knightrider", "type": "Person" }, "target": { "external_id": "kitt", "type": "Car" }, "type": "DRIVES" }, { "source": { "external_id": "satchmo", "type": "Person" }, "target": { "external_id": "cadillacv16", "type": "Car" }, "type": "DRIVES" }, { "source": { "external_id": "karel", "type": "Person" }, "target": { "external_id": "listek", "type": "Ticket" }, "type": "HAS" }, { "source": { "external_id": "listek", "type": "Ticket" }, "target": { "external_id": "harmonika", "type": "Bus" }, "type": "FOR" }, { "source": { "external_id": "karel", "type": "Person" }, "target": { "external_id": "airbook-xyz", "type": "Laptop" }, "type": "OWNS" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "airbook-xyz", "type": "Laptop" }, "type": "OWNS" }, { "source": { "external_id": "knightrider", "type": "Person" }, "target": { "external_id": "kitt", "type": "Car" }, "type": "OWNS" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "cadillacv16", "type": "Car" }, "type": "DRIVES" } ] } ``` ### Step 2 KBAC Policy which rules that a Person node can drive a car if the Person node has a relation DRIVES with a Car node. **policy.json** ```json { "meta": { "policy_version": "2.0-kbac" }, "subject": { "type": "Person" }, "actions": [ "CAN_DRIVE" ], "resource": { "type": "Car" }, "condition": { "cypher": "MATCH (subject:Person)-[:DRIVES]->(resource:Car)" } } ``` Request to create the KBAC Policy configuration using REST. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"meta\":{\"policy_version\":\"2.0-kbac\"},\"subject\":{\"type\":\"Person\"},\"actions\":[\"CAN_DRIVE\"],\"resource\":{\"type\":\"Car\"},\"condition\":{\"cypher\":\"MATCH (subject:Person)-[:DRIVES]->(resource:Car)\"}}", "status": "ACTIVE", "tags": [] } ``` Request to create the KBAC Policy configuration using Python. **policy_request.py** ```python import http.client conn = http.client.HTTPSConnection("eu.api.indykite.com") payload = "{"description": "", "display_name": "", "name": "", "policy": "", "project_id": "", "status": "ACTIVE", "tags": [ "" ]}" headers = { 'Content-Type': "application/json", 'Authorization': "YOUR_SECRET_TOKEN" } conn.request("POST", "/configs/v1/authorization-policies", payload, headers) res = conn.getresponse() data = res.read() ``` Request to read the KBAC Policy configuration using REST. **GET https://eu.api.indykite.com/configs/v1/authorization-policies/{id}** ```json { "id": "your_policy_configuration_gid" } ``` Request to read the KBAC Policy configuration using Python. **policy_request.py** ```python import http.client conn = http.client.HTTPSConnection("eu.api.indykite.com") headers = { 'Authorization': "YOUR_SECRET_TOKEN" } conn.request("GET", "/configs/v1/authorization-policies/{{id}}", headers=headers) res = conn.getresponse() data = res.read() ``` ### Step 3 Request to run a KBAC authZEN Evaluation - authorized. **POST https://eu.api.indykite.com/access/v1/evaluation** ```json { "subject": { "type": "Person", "id": "knightrider" }, "resource": { "type": "Car", "id": "kitt" }, "action": { "name": "CAN_DRIVE" } } ``` Response to the KBAC authZEN Evaluation - authorized. **Response 200** ```json { "decision": true } ``` Request to run a KBAC authZEN Evaluation - authorized. **evaluation.py** ```python import http.client conn = http.client.HTTPSConnection("eu.api.indykite.com") payload = "{ "subject": {"type": "Person", "id": "knightrider"}, "resource": {"type": "Car", "id": "kitt"}, "action": {"name": "CAN_DRIVE"} }" headers = { 'Content-Type': "application/json", 'X-IK-ClientKey': "" } conn.request("POST", "/access/v1/evaluation", payload, headers) res = conn.getresponse() data = res.read() ``` Request to run a KBAC authZEN Evaluation - not authorized. **POST https://eu.api.indykite.com/access/v1/evaluation** ```json { "subject": { "type": "Person", "id": "knightrider" }, "resource": { "type": "Car", "id": "caddilacv16" }, "action": { "name": "CAN_DRIVE" } } ``` Response to the KBAC authZEN Evaluation - not authorized. **Response 200** ```json { "decision": false } ``` ### Step 4 Request to run a KBAC authZEN Action Search. **POST https://eu.api.indykite.com/access/v1/search/action** ```json { "subject": { "type": "Person", "id": "knightrider" }, "resource": { "type": "Car", "id": "kitt" } } ``` Response to the KBAC authZEN Action Search. **Response 200** ```json { "results": [ { "name": "CAN_DRIVE" } ] } ``` Request to run a KBAC authZEN Action Search using Python. **action.py** ```python import http.client conn = http.client.HTTPSConnection("eu.api.indykite.com") payload = "{ "subject": {"type": "Person", "id": "knightrider"}, "resource": {"type": "Car", "id": "kitt"} }" headers = { 'Content-Type': "application/json", 'X-IK-ClientKey': "" } conn.request("POST", "/access/v1/search/action", payload, headers) res = conn.getresponse() data = res.read() ``` Request to run a KBAC authZEN Resource Search. **POST https://eu.api.indykite.com/access/v1/search/resource** ```json { "subject": { "type": "Person", "id": "knightrider" }, "resource": { "type": "Car" }, "action": { "name": "CAN_DRIVE" } } ``` Response to the KBAC authZEN Resource Search. **Response 200** ```json { "results": [ { "type": "Car", "id": "kitt" } ] } ``` Request to run a KBAC authZEN Resource Search using Python. **resource.py** ```python import http.client conn = http.client.HTTPSConnection("eu.api.indykite.com") payload = "{ "subject": {"type": "Person", "id": "knightrider"}, "resource": {"type": "Car"}, "action": {"name": "CAN_DRIVE"} }" headers = { 'Content-Type': "application/json", 'X-IK-ClientKey': "" } conn.request("POST", "/access/v1/search/resource", payload, headers) res = conn.getresponse() data = res.read() ``` Request to run a KBAC authZEN Subject Search. **POST https://eu.api.indykite.com/access/v1/search/subject** ```json { "subject": { "type": "Person" }, "resource": { "type": "Car", "id": "kitt" }, "action": { "name": "CAN_DRIVE" } } ``` Response to the KBAC authZEN Subject Search. **Response 200** ```json { "results": [ { "type": "Person", "id": "knightrider" } ] } ``` Request to run a KBAC authZEN Subject Search using Python. **subject.py** ```python import http.client conn = http.client.HTTPSConnection("eu.api.indykite.com") payload = "{ "subject": {"type": "Person"}, "resource": {"type": "Car", "id": "kitt"}, "action": {"name": "CAN_DRIVE"} }" headers = { 'Content-Type': "application/json", 'X-IK-ClientKey': "" } conn.request("POST", "/access/v1/search/subject", payload, headers) res = conn.getresponse() data = res.read() ``` ### Step 5 Delete the KBAC Policy. **DELETE https://eu.api.indykite.com/configs/v1/authorization-policies/{id}** ```json { "id": "your_policy_configuration_gid" } ``` Request to delete the KBAC Policy. **del.py** ```python import http.client conn = http.client.HTTPSConnection("eu.api.indykite.com") headers = { 'Authorization': "Bearer ...", 'Content-Type': "application/json" } conn.request("DELETE", "/configs/v1/authorization-policies/{id}", headers=headers) res = conn.getresponse() data = res.read() ``` ## Common Errors ### 401: UNAUTHENTICATED **Solution:** Check X-IK-ClientKey header contains valid AppAgent credentials ### 404: NOT_FOUND **Solution:** Verify the policy exists and the subject/resource nodes exist in the IKG --- Source: https://developer.indykite.com/resources/authz-1 --- # KBAC: Batch Authorization with Multiple Evaluations (Boxcarring) > Evaluate multiple authorization decisions in a single API call using the authZEN Access Evaluations endpoint. This pattern, known as 'boxcarring', reduces network overhead when checking many permissions at once. **Category:** KBAC **API:** KBAC **Tags:** KBAC Policy, authZEN, Batch Evaluation, Boxcarring, Multi-Evaluation, Performance Optimization **Last Updated:** 2026-07-14 **OpenAPI Endpoints:** /capture/v1/nodes, /capture/v1/relationships, /configs/v1/authorization-policies, /access/v1/evaluations **Related Guides:** /guides/guide-authzen, /guides/guide-dynamic-authz, /guides/guide-sandbox ## Summary This example demonstrates batch authorization using the authZEN Access Evaluations API: What is boxcarring? Multiple authorization questions combined into a single API request, reducing network round-trips. Example batch request: - "Can Alice drive Car1?" - "Can Alice ride Bus1?" - "Can Bob drive Car1?" All answered in ONE API call. API reference: https://openid.net/specs/authorization-api-1_0-03.html#name-access-evaluations-api Request structure: - Default subject/action can be set at the top level - Each evaluation in the array can override defaults - Response contains decision for each evaluation in order Policy version note: the policies in this example use "policy_version": "2.0-kbac". The same policy JSON is also accepted with "policy_version": "3.0-kbac" (identical schema, raw-Cypher semantics; do not reference $subject_id or external properties). Only 3.0-kbac additionally accepts USE graph.byName() routing and CALL { } subqueries for composite / data-residency IKGs - see resources authz-7 and authz-8. ## Use Case Scenario: A transportation app needs to check multiple permissions when a user opens the app. Policies defined: 1. DRIVE policy: Person can DRIVE Car if DRIVES relationship exists 2. RIDE policy: Person can RIDE Bus if HAS_TICKET relationship exists Single batch request checks: - Can Alice drive Car1? (uses DRIVE policy) - Can Alice drive Car2? (uses DRIVE policy) - Can Alice ride Bus1? (uses RIDE policy) Response (in order): - {decision: true} - {decision: false} - {decision: true} Performance benefit: 3 authorization decisions with 1 API call instead of 3. ## Requirements Prerequisites: - ServiceAccount credentials: For creating policies (Bearer token) - AppAgent credentials: For data ingestion and authorization queries (X-IK-ClientKey) Required API access: - POST /capture/v1/nodes/ and /capture/v1/relationships/ (graph data) - POST /configs/v1/authorization-policies (create policies - called twice) - POST /access/v1/evaluations (batch evaluation endpoint - note plural) ## Steps Step 1: Ingest Graph Data - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: POST nodes (Person, Car, Bus) and relationships (DRIVES, HAS_TICKET) - Result: Graph ready for authorization queries Step 2: Create Multiple KBAC Policies - Authentication: ServiceAccount credential (Bearer token) - Action: POST DRIVE policy (Person -[DRIVES]-> Car = can drive) - Action: POST RIDE policy (Person -[HAS_TICKET]-> Bus = can ride) - Result: Two policy IDs returned Step 3: Run Batch Evaluation - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: POST to /access/v1/evaluations (plural) with array of checks - Request format: { subject: {type: "Person", id: "alice"}, // default subject evaluations: [ {action: {name: "drive"}, resource: {type: "Car", id: "car1"}}, {action: {name: "drive"}, resource: {type: "Car", id: "car2"}}, {action: {name: "ride"}, resource: {type: "Bus", id: "bus1"}} ] } - Result: Array of decisions matching input order Step 5: Cleanup - Action: DELETE both policy configurations ## Code Examples ### Step 1 Capture the nodes needed for this use case. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "alice", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "alice@email.com" }, { "type": "given_name", "value": "Alice" }, { "type": "last_name", "value": "Smith" } ] }, { "external_id": "ryan", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "ryan@yahoo.co.uk" }, { "type": "given_name", "value": "ryan" }, { "type": "last_name", "value": "mushu" } ] }, { "external_id": "tilda", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "tilda@yahoo.co.uk" }, { "type": "given_name", "value": "tilda" }, { "type": "last_name", "value": "mushu" } ] }, { "external_id": "cb123", "type": "PaymentMethod", "properties": [ { "type": "payment_name", "value": "Credit Card" } ] }, { "external_id": "kl123", "type": "PaymentMethod", "properties": [ { "type": "payment_name", "value": "Klarna" } ] }, { "external_id": "ct123", "type": "Contract", "properties": [ { "type": "category", "value": "Insurance" }, { "type": "status", "value": "Active" }, { "type": "number", "value": "hfgrten123", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "ct234", "type": "Contract", "properties": [ { "type": "category", "value": "Insurance" }, { "type": "status", "value": "Active" }, { "type": "number", "value": "hfgrten234", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "ct985", "type": "Contract", "properties": [ { "type": "category", "value": "Insurance" }, { "type": "status", "value": "Active" }, { "type": "number", "value": "hfgrten985", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "car1", "type": "Vehicle", "properties": [ { "type": "category", "value": "Car" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "rtfhcnvjt471" } ] }, { "external_id": "car2", "type": "Vehicle", "properties": [ { "type": "category", "value": "Car" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "kdcbfrt178" } ] }, { "external_id": "truck1", "type": "Vehicle", "properties": [ { "type": "category", "value": "Truck" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "sncnrkcldp" } ] }, { "external_id": "license1", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "AX123456", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "license2", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "OL123456", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "license3", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "VN123456", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "company1", "type": "Company", "properties": [ { "type": "name", "value": "Company1" }, { "type": "registration", "value": "256314523" } ] }, { "external_id": "company2", "type": "Company", "properties": [ { "type": "name", "value": "Company2" }, { "type": "registration", "value": "942365123" } ] }, { "external_id": "application1", "type": "Application", "properties": [ { "type": "name", "value": "Application" } ] }, { "external_id": "application2", "type": "Application", "properties": [ { "type": "name", "value": "Application2" } ] } ] } ``` Capture the relationships needed for this use case. **POST https://eu.api.indykite.com/capture/v1/relationships/** ```json { "relationships": [ { "source": { "external_id": "ryan", "type": "Person" }, "target": { "external_id": "cb123", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "tilda", "type": "Person" }, "target": { "external_id": "kl123", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "cb123", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "ryan", "type": "Person" }, "target": { "external_id": "ct123", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "tilda", "type": "Person" }, "target": { "external_id": "ct234", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "ct985", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "ct123", "type": "Contract" }, "target": { "external_id": "car1", "type": "Vehicle" }, "type": "COVERS" }, { "source": { "external_id": "ct985", "type": "Contract" }, "target": { "external_id": "car1", "type": "Vehicle" }, "type": "COVERS" }, { "source": { "external_id": "ct234", "type": "Contract" }, "target": { "external_id": "truck1", "type": "Vehicle" }, "type": "COVERS" }, { "source": { "external_id": "car1", "type": "Vehicle" }, "target": { "external_id": "license1", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "truck1", "type": "Vehicle" }, "target": { "external_id": "license2", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "car2", "type": "Vehicle" }, "target": { "external_id": "license3", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "company1", "type": "Company" }, "target": { "external_id": "car1", "type": "Vehicle" }, "type": "OWNS" }, { "source": { "external_id": "company1", "type": "Company" }, "target": { "external_id": "car2", "type": "Vehicle" }, "type": "OWNS" }, { "source": { "external_id": "company1", "type": "Company" }, "target": { "external_id": "truck1", "type": "Vehicle" }, "type": "OWNS" }, { "source": { "external_id": "application1", "type": "Application" }, "target": { "external_id": "company1", "type": "Company" }, "type": "HAS_AGREEMENT_WITH" }, { "source": { "external_id": "application2", "type": "Application" }, "target": { "external_id": "company1", "type": "Company" }, "type": "HAS_AGREEMENT_WITH" } ] } ``` ### Step 2 KBAC Policy which rules that a Person node can drive a car if the Person node has a relation DRIVES with a car in json format. **policy.json** ```json { "meta": { "policy_version": "2.0-kbac" }, "subject": { "type": "Person" }, "actions": [ "CAN_DRIVE" ], "resource": { "type": "Car" }, "condition": { "cypher": "MATCH (subject:Person)-[:DRIVES]->(resource:Car)" } } ``` Request to create the KBAC Policy configuration using REST. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"meta\":{\"policy_version\":\"2.0-kbac\"},\"subject\":{\"type\":\"Person\"},\"actions\":[\"CAN_DRIVE\"],\"resource\":{\"type\":\"Car\"},\"condition\":{\"cypher\":\"MATCH (subject:Person)-[:DRIVES]->(resource:Car)\"}}", "status": "ACTIVE", "tags": [] } ``` Request to create the KBAC Policy configuration using Python. **policy_request.py** ```python import http.client conn = http.client.HTTPSConnection("eu.api.indykite.com") payload = "{"description": "", "display_name": "", "name": "", "policy": "", "project_id": "", "status": "ACTIVE", "tags": [ "" ]}" headers = { 'Content-Type': "application/json", 'Authorization': "YOUR_SECRET_TOKEN" } conn.request("POST", "/configs/v1/authorization-policies", payload, headers) res = conn.getresponse() data = res.read() ``` Request to read the KBAC Policy configuration using REST **GET https://eu.api.indykite.com/configs/v1/authorization-policies/{id}** ```json { "id": "your_policy_configuration_gid" } ``` Request to read the KBAC Policy configuration using Python. **policy_request.py** ```python import http.client conn = http.client.HTTPSConnection("eu.api.indykite.com") headers = { 'Authorization': "YOUR_SECRET_TOKEN" } conn.request("GET", "/configs/v1/authorization-policies/{{id}}", headers=headers) res = conn.getresponse() data = res.read() ``` KBAC Policy which rules that a Person node can ride a bus if the Person node has a ticket for the bus in json format. **policy.json** ```json { "meta": { "policy_version": "2.0-kbac" }, "subject": { "type": "Person" }, "actions": [ "CAN_RIDE" ], "resource": { "type": "Bus" }, "condition": { "cypher": "MATCH (subject)-[:HAS]->(ticket:Ticket)-[:FOR]->(resource)" } } ``` Request to create the KBAC Policy configuration using REST. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"meta\":{\"policy_version\":\"2.0-kbac\"},\"subject\":{\"type\":\"Person\"},\"actions\":[\"CAN_RIDE\"],\"resource\":{\"type\":\"Bus\"},\"condition\":{\"cypher\":\"MATCH (subject)-[:HAS]->(ticket:Ticket)-[:FOR]->(resource)\"}}", "status": "ACTIVE", "tags": [] } ``` Request to create the KBAC Policy configuration using Python. **policy_request.py** ```python import http.client conn = http.client.HTTPSConnection("eu.api.indykite.com") payload = "{"description": "", "display_name": "", "name": "", "policy": "", "project_id": "", "status": "ACTIVE", "tags": [ "" ]}" headers = { 'Content-Type': "application/json", 'Authorization': "YOUR_SECRET_TOKEN" } conn.request("POST", "/configs/v1/authorization-policies", payload, headers) res = conn.getresponse() data = res.read() ``` Request to read the KBAC Policy configuration using REST. **GET https://eu.api.indykite.com/configs/v1/authorization-policies/{id}** ```json { "id": "your_policy_configuration_gid" } ``` Request to read the KBAC Policy configuration using Python. **policy_request.py** ```python import http.client conn = http.client.HTTPSConnection("eu.api.indykite.com") headers = { 'Authorization': "YOUR_SECRET_TOKEN" } conn.request("GET", "/configs/v1/authorization-policies/{{id}}", headers=headers) res = conn.getresponse() data = res.read() ``` ### Step 3 Json to run KBAC access multi-evaluations. The elements outside evaluations are the default values. Here we have a default value for subject and for action. If a top-level key is designated in the evaluations array then the value of that will take precedence over the default value. **POST https://eu.api.indykite.com/access/v1/evaluations** ```json { "subject": { "type": "Person", "id": "karel" }, "action": { "name": "CAN_DRIVE" }, "evaluations": [ { "subject": { "type": "Person", "id": "knightrider" }, "resource": { "type": "Car", "id": "kitt" } }, { "subject": { "type": "Person", "id": "knightrider" }, "resource": { "type": "Car", "id": "caddilacv16" } }, { "resource": { "type": "Bus", "id": "harmonika" }, "action": { "name": "CAN_RIDE" } }, { "resource": { "type": "Bus", "id": "harmonika" } } ] } ``` Response to the KBAC Evaluations endpoint request. **Response 200** ```json { "evaluations": [ { "decision": true }, { "decision": false }, { "decision": true }, { "decision": false } ] } ``` Request to run the KBAC Evaluations endpoint. **evaluations.py** ```python import http.client conn = http.client.HTTPSConnection("eu.api.indykite.com") payload = "{ "subject": {"type": "Person", "id": "karel"}, "action": {"name": "CAN_DRIVE"}, "evaluations": [ { "subject": {"type": "Person", "id": "knightrider"}, "resource": {"type": "Car", "id": "kitt"} }, { "subject": {"type": "Person", "id": "knightrider"}, "resource": {"type": "Car", "id": "caddilacv16"} }, { "resource": {"type": "Bus", "id": "harmonika"}, "action": {"name": "CAN_RIDE"} }, { "resource": {"type": "Bus", "id": "harmonika"} } ] }" headers = { 'Content-Type': "application/json", 'X-IK-ClientKey': "" } conn.request("POST", "/access/v1/evaluations", payload, headers) res = conn.getresponse() data = res.read() ``` ### Step 5 Delete the KBAC Policies. **DELETE https://eu.api.indykite.com/configs/v1/authorization-policies/{id}** ```json { "id": "your_policy_configuration_gid" } ``` Request to delete the KBAC Policies. **del.py** ```python import http.client conn = http.client.HTTPSConnection("eu.api.indykite.com") headers = { 'Authorization': "Bearer ...", 'Content-Type': "application/json" } conn.request("DELETE", "/configs/v1/authorization-policies/{id}", headers=headers) res = conn.getresponse() data = res.read() ``` ## Common Errors ### 401: UNAUTHENTICATED **Solution:** Check X-IK-ClientKey header contains valid AppAgent credentials ### 404: NOT_FOUND **Solution:** Verify the policy exists and the subject/resource nodes exist in the IKG --- Source: https://developer.indykite.com/resources/authz-2 --- # KBAC: Step-Up Authentication Advice in Authorization Responses > Demonstrates authZEN 'advice' - when authorization is denied due to insufficient authentication level, the response includes guidance on what authentication step-up is needed to gain access. **Category:** KBAC **API:** KBAC **Tags:** KBAC Policy, authZEN, Step-Up Authentication, Advice, MFA, Authentication Level, Conditional Access **Last Updated:** 2026-07-14 **OpenAPI Endpoints:** /capture/v1/nodes, /capture/v1/relationships, /configs/v1/authorization-policies, /access/v1/evaluation **Related Guides:** /guides/guide-authzen, /guides/guide-dynamic-authz, /guides/guide-sandbox ## Summary This example demonstrates authZEN "advice" responses for step-up authentication: What is advice? When authorization is denied, the response can include guidance on HOW to get access (not just "denied"). Example flow: 1. User tries to access sensitive resource with basic authentication 2. KBAC denies access but returns advice: "Requires MFA authentication" 3. Application prompts user for MFA 4. User re-authenticates with MFA 5. Same request now succeeds This enables progressive authentication - users only need stronger auth for sensitive operations. Policy version note: the policy in this example uses "policy_version": "2.0-kbac". The same policy JSON is also accepted with "policy_version": "3.0-kbac" (identical schema, raw-Cypher semantics; do not reference $subject_id or external properties). Only 3.0-kbac additionally accepts USE graph.byName() routing and CALL { } subqueries for composite / data-residency IKGs - see resources authz-7 and authz-8. ## Use Case Scenario: A user wants to service their laptop (e.g., wipe data, change settings). Policy conditions: 1. Person must OWN the Laptop (relationship check) 2. Person's authentication token must not be expired (time check) 3. Person's subject.external_id must match the token's "sub" claim (identity verification) Without proper authentication: - Request: "Can Alice service Laptop1?" - Response: {decision: false, advice: {step_up: "mfa_required"}} With proper authentication (MFA token): - Request: "Can Alice service Laptop1?" (with valid MFA token) - Response: {decision: true} The advice object tells the application exactly what authentication upgrade is needed. ## Requirements Prerequisites: - ServiceAccount credentials: For creating policies (Bearer token) - AppAgent credentials: For data ingestion and authorization queries (X-IK-ClientKey) - Auth0 (or similar) user token: For the user requiring access Required API access: - POST /capture/v1/nodes/ and /capture/v1/relationships/ (graph data) - POST /configs/v1/authorization-policies (create policy) - POST /access/v1/evaluation (authorization check with advice) ## Steps Step 1: Ingest Graph Data - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: POST Person and Laptop nodes with OWNS relationship - Result: Graph ready for authorization queries Step 2: Create Step-Up Policy - Authentication: ServiceAccount credential (Bearer token) - Action: POST policy with conditions: - Person must OWN the Laptop - Token "sub" claim must match Person.external_id - Token must not be expired - Policy includes advice configuration for step-up scenarios - Result: Policy ID returned Step 3: Evaluation Without Proper Auth (Returns Advice) - Authentication: AppAgent credential (X-IK-ClientKey header) - Request: Evaluate "Can Alice service Laptop1?" without user token - Response: {decision: false, advice: {action: "step_up", auth_level: "mfa"}} - The advice tells the app to request MFA from the user Step 4: Evaluation With Proper Auth (Grants Access) - Authentication: AppAgent credential + User's MFA token (Bearer header) - Request: Same evaluation request, but now with valid MFA token - Response: {decision: true} - User is now authorized because token claims satisfy policy conditions Step 5: Cleanup - Action: DELETE policy configuration ## Code Examples ### Step 1 Capture the nodes needed for this use case. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "alice", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "alice@email.com" }, { "type": "given_name", "value": "Alice" }, { "type": "last_name", "value": "Smith" } ] }, { "external_id": "knightrider", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "knightrider@demo.com" }, { "type": "name", "value": "Michael Knight" } ] }, { "external_id": "satchmo", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "satchmo@demo.com" }, { "type": "name", "value": "Louis Armstrong" } ] }, { "external_id": "karel", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "karel@demo.com" }, { "type": "name", "value": "Karel Plihal" } ] }, { "external_id": "kitt", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "pontiac" }, { "type": "model", "value": "Firebird" } ] }, { "external_id": "cadillacv16", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "Cadillac" }, { "type": "model", "value": "V-16" } ] }, { "external_id": "harmonika", "type": "Bus", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "Ikarus" }, { "type": "model", "value": "280" } ] }, { "external_id": "listek", "type": "Ticket", "is_identity": false }, { "external_id": "airbook-xyz", "type": "Laptop", "is_identity": false } ] } ``` Capture the relationships needed for this use case. **POST https://eu.api.indykite.com/capture/v1/relationships/** ```json { "relationships": [ { "source": { "external_id": "knightrider", "type": "Person" }, "target": { "external_id": "kitt", "type": "Car" }, "type": "DRIVES" }, { "source": { "external_id": "satchmo", "type": "Person" }, "target": { "external_id": "cadillacv16", "type": "Car" }, "type": "DRIVES" }, { "source": { "external_id": "karel", "type": "Person" }, "target": { "external_id": "listek", "type": "Ticket" }, "type": "HAS" }, { "source": { "external_id": "listek", "type": "Ticket" }, "target": { "external_id": "harmonika", "type": "Bus" }, "type": "FOR" }, { "source": { "external_id": "karel", "type": "Person" }, "target": { "external_id": "airbook-xyz", "type": "Laptop" }, "type": "OWNS" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "airbook-xyz", "type": "Laptop" }, "type": "OWNS" }, { "source": { "external_id": "knightrider", "type": "Person" }, "target": { "external_id": "kitt", "type": "Car" }, "type": "OWNS" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "cadillacv16", "type": "Car" }, "type": "DRIVES" } ] } ``` ### Step 2 KBAC Policy which rules that a Person node can service a laptop if the Person node owns it and if the external_id of the subject Person node is equal to the token sub. **policy.json** ```json { "meta": { "policy_version": "2.0-kbac" }, "subject": { "type": "Token" }, "actions": [ "CAN_SERVICE" ], "resource": { "type": "Laptop" }, "condition": { "cypher": "MATCH (subject:Token)-[:_SAME_AS]->(person:Person)-[:OWNS]->(resource)", "filter": { "attribute": "$token.exp", "operator": ">", "value": 1789571588, "advice": { "error": "insufficient_user_authentication", "error_description": "Authentication is expired", "sub": "$token.exp" } } } } ``` Request to create the KBAC Policy configuration using REST. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"meta\":{\"policy_version\":\"2.0-kbac\"},\"subject\":{\"type\":\"Token\"},\"actions\":[\"CAN_SERVICE\"],\"resource\":{\"type\":\"Laptop\"},\"condition\":{\"cypher\":\"MATCH (subject:Token)-[:_SAME_AS]->(person:Person)-[:OWNS]->(resource)\",\"filter\":{\"attribute\":\"$token.exp\",\"operator\":\">\",\"value\":1789571588,\"advice\":{\"error\":\"insufficient_user_authentication\",\"error_description\":\"Authentication is expired\",\"sub\":\"$token.exp\"}}}}", "status": "ACTIVE", "tags": [] } ``` Request to create the KBAC Policy configuration using REST. **policy_request.py** ```python import http.client conn = http.client.HTTPSConnection("eu.api.indykite.com") payload = "{"description": "", "display_name": "", "name": "", "policy": "", "project_id": "", "status": "ACTIVE", "tags": [ "" ]}" headers = { 'Content-Type': "application/json", 'Authorization': "YOUR_SECRET_TOKEN" } conn.request("POST", "/configs/v1/authorization-policies", payload, headers) res = conn.getresponse() data = res.read() ``` Request to read the KBAC Policy configuration using REST. **GET https://eu.api.indykite.com/configs/v1/authorization-policies/{id}** ```json { "id": "your_policy_configuration_gid" } ``` Request to read the KBAC Policy configuration using REST. **policy_request.py** ```python import http.client conn = http.client.HTTPSConnection("eu.api.indykite.com") headers = { 'Authorization': "YOUR_SECRET_TOKEN" } conn.request("GET", "/configs/v1/authorization-policies/{{id}}", headers=headers) res = conn.getresponse() data = res.read() ``` ### Step 3 Run a KBAC authZEN Evaluation which returns step up. **POST /access/v1/evaluations** ```json { "subject": { "type": "Token", "id": "token_sub_value" }, "resource": { "type": "Laptop", "id": "airbook-xyz" }, "action": { "name": "CAN_SERVICE" } } ``` Request to run KBAC authZEN Evaluation which returns step up. **evaluations.py** ```python import http.client conn = http.client.HTTPSConnection("eu.api.indykite.com") payload = "{ "subject": {"type": "Token", "id": "token_sub_value"}, "resource": {"type": "Laptop", "id": "airbook-xyz"}, "action": {"name": "CAN_SERVICE"} }" headers = { 'Content-Type': "application/json", 'Authorization': "Bearer eyJh....", 'X-IK-ClientKey': "eyJh...." } conn.request("POST", "/access/v1/evaluation", payload, headers) res = conn.getresponse() data = res.read() ``` Response to the KBAC authZEN Evaluation request. **Response 200** ```json { "context": { "advice": [ { "error": "insufficient_user_authentication", "error_description": "Authentication is expired", "sub": "$token.exp" } ] }, "decision": false } ``` ### Step 4 Add the Person node access token in the headers: key:Authorization value: Bearer access_token_value Then run the same KBAC authZEN Evaluation. **POST https://eu.api.indykite.com/access/v1/evaluations** ```json { "subject": { "type": "Token", "id": "token_sub_value" }, "resource": { "type": "Laptop", "id": "airbook-xyz" }, "action": { "name": "CAN_SERVICE" } } ``` Response to the KBAC authZEN Evaluation request. **Response 200** ```json { "decision": true } ``` ### Step 5 Delete the KBAC Policies. **DELETE https://eu.api.indykite.com/configs/v1/authorization-policies/{id}** ```json { "id": "your_policy_configuration_gid" } ``` Request to delete the KBAC Policies. **del.py** ```python import http.client conn = http.client.HTTPSConnection("eu.api.indykite.com") headers = { 'Authorization': "Bearer ...", 'Content-Type': "application/json" } conn.request("DELETE", "/configs/v1/authorization-policies/{id}", headers=headers) res = conn.getresponse() data = res.read() ``` ## Common Errors ### 401: UNAUTHENTICATED **Solution:** Check X-IK-ClientKey header contains valid AppAgent credentials ### 404: NOT_FOUND **Solution:** Verify the policy exists and the subject/resource nodes exist in the IKG --- Source: https://developer.indykite.com/resources/authz-3 --- # KBAC: Parameterized Authorization with Input Params (max_price) > Drive authZEN evaluation and resource-search decisions using runtime input_params. The policy condition compares a graph property against $max_price provided at execution time, so the same policy answers different shopping-budget questions per call. **Category:** KBAC **API:** KBAC **Tags:** KBAC Policy, authZEN, Input Params, Parameterized Policy, Evaluation, Resource Search, Subject Search **Last Updated:** 2026-07-14 **OpenAPI Endpoints:** /configs/v1/authorization-policies, /access/v1/evaluation, /access/v1/search/resource, /access/v1/search/subject **Related Guides:** /guides/guide-dynamic-authz, /guides/guide-authzen ## Summary This example demonstrates input_params passed through context on authZEN requests. Policy condition shape: MATCH (subject) MATCH (resource)-[:HAS]->(price:Property {type:'price'}) WHERE price.value <= $max_price AND NOT (subject)-[:OWNS]->(resource) Two things matter: 1. $max_price is a partial filter - its value is supplied per request via context.input_params. 2. The NOT clause excludes cars the subject already owns, which keeps the answer truthful for "what can I buy?" style questions. authZEN endpoints exercised: - POST /access/v1/evaluation - "Can Karel buy KITT under $150,000?" -> true. Same question under $5,000 -> false. - POST /access/v1/search/resource - "Which cars can Knight Rider buy under $5,000?" -> [{skodaOctavia}]. - POST /access/v1/search/subject - "Who can buy the Skoda Octavia under $5,000?" -> [knightrider, karel]. Policy version note: the policy in this example uses "policy_version": "2.0-kbac". The same policy JSON is also accepted with "policy_version": "3.0-kbac" (identical schema, raw-Cypher semantics; do not reference $subject_id or external properties). Only 3.0-kbac additionally accepts USE graph.byName() routing and CALL { } subqueries for composite / data-residency IKGs - see resources authz-7 and authz-8. ## Use Case Scenario: A used-car marketplace asks IndyKite "what can this shopper afford right now?" with a moving budget cap. Graph data: - Cars carry a HAS -> Property{type:'price'} relationship with the listing price. - Subjects OWN the cars they already bought (excluded from CAN_BUY results). Run the same policy three ways: 1. evaluation with max_price=150000 -> Karel can buy KITT. 2. evaluation with max_price=5000 -> Karel cannot buy KITT. 3. search/resource with max_price=5000 -> only cars under $5,000 come back. Use this pattern whenever the authorization rule depends on a numeric threshold supplied by the caller - discounts, credit limits, rate-limit budgets, etc. ## Requirements Prerequisites: - ServiceAccount credentials: For creating the policy. - AppAgent credentials: For evaluations and searches (X-IK-ClientKey). - Graph data: Person, Car, and Property{type:'price'} nodes with HAS relationships from each Car to its price Property. OWNS relationships for any cars already owned. Required API access: - POST /configs/v1/authorization-policies - POST /access/v1/evaluation - POST /access/v1/search/resource - POST /access/v1/search/subject ## Steps Step 1: Capture Graph Data - Action: POST Person, Car, Property nodes; HAS and OWNS relationships. - Result: Cars priced, owners linked. Step 2: Create the Parameterized Policy - Action: POST a KBAC policy with $max_price in the condition cypher. - Result: Policy ID. Step 3: Evaluate with Different Budgets - Action: POST /access/v1/evaluation with context.input_params.max_price set. - Result: decision flips based on price vs. budget. Step 4: Search for Affordable Resources - Action: POST /access/v1/search/resource with max_price in context. - Result: All cars under the budget that the subject doesn't already own. Step 5: Reverse Search (Who Can Buy This?) - Action: POST /access/v1/search/subject with max_price in context. - Result: All Persons who could afford the resource and don't already own it. ## Code Examples ### Step 1 Capture Person and Car nodes. Each Car carries a price property (kitt 100000, caddilacv16 150000, skodaOctavia 4000) — captured as an integer so the policy can compare price.value <= $max_price. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "knightrider", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "knightrider@demo.com" } ] }, { "external_id": "karel", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "karel@demo.com" } ] }, { "external_id": "kitt", "type": "Car", "properties": [ { "type": "manufacturer", "value": "Pontiac" }, { "type": "model", "value": "Firebird" }, { "type": "price", "value": 100000 } ] }, { "external_id": "caddilacv16", "type": "Car", "properties": [ { "type": "manufacturer", "value": "Cadillac" }, { "type": "model", "value": "V-16" }, { "type": "price", "value": 150000 } ] }, { "external_id": "skodaOctavia", "type": "Car", "properties": [ { "type": "manufacturer", "value": "Skoda" }, { "type": "model", "value": "Octavia" }, { "type": "price", "value": 4000 } ] } ] } ``` Capture relationships: knightrider OWNS kitt, which the policy's NOT (subject)-[:OWNS]->(resource) clause uses to exclude cars the subject already owns. **POST https://eu.api.indykite.com/capture/v1/relationships/** ```json { "relationships": [ { "source": { "external_id": "knightrider", "type": "Person" }, "target": { "external_id": "kitt", "type": "Car" }, "type": "OWNS" } ] } ``` ### Step 2 KBAC policy: CAN_BUY a Car if its price is <= $max_price and the subject does not already own it. **policy.json** ```json { "meta": { "policy_version": "2.0-kbac" }, "subject": { "type": "Person" }, "actions": [ "CAN_BUY" ], "resource": { "type": "Car" }, "condition": { "cypher": "MATCH (subject) MATCH (resource)-[:HAS]->(price:Property {type:'price'}) WHERE price.value <= $max_price AND NOT (subject)-[:OWNS]->(resource)" } } ``` Request to create the policy. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "Authorize CAN_BUY on Car when its price <= input_params.max_price and the subject does not already own it.", "display_name": "policy - person can buy car under max_price", "name": "policy-person-can-buy-car-max-price", "policy": "{\"meta\":{\"policy_version\":\"2.0-kbac\"},\"subject\":{\"type\":\"Person\"},\"actions\":[\"CAN_BUY\"],\"resource\":{\"type\":\"Car\"},\"condition\":{\"cypher\":\"MATCH (subject) MATCH (resource)-[:HAS]->(price:Property {type:'price'}) WHERE price.value <= $max_price AND NOT (subject)-[:OWNS]->(resource)\"}}", "status": "ACTIVE", "tags": [] } ``` ### Step 3 Evaluation: Karel asks if he can buy KITT with budget 150,000. Decision: true. **POST https://eu.api.indykite.com/access/v1/evaluation** ```json { "subject": { "type": "Person", "id": "karel" }, "resource": { "type": "Car", "id": "kitt" }, "action": { "name": "CAN_BUY" }, "context": { "input_params": { "max_price": 150000 } } } ``` Response - authorized. **Response 200** ```json { "decision": true } ``` Same call, budget dropped to 5,000. Decision: false. **POST https://eu.api.indykite.com/access/v1/evaluation** ```json { "subject": { "type": "Person", "id": "karel" }, "resource": { "type": "Car", "id": "kitt" }, "action": { "name": "CAN_BUY" }, "context": { "input_params": { "max_price": 5000 } } } ``` Response - denied because KITT's price exceeds 5,000. **Response 200** ```json { "decision": false } ``` ### Step 4 Resource search: which cars can Knight Rider buy under 5,000? **POST https://eu.api.indykite.com/access/v1/search/resource** ```json { "subject": { "type": "Person", "id": "knightrider" }, "resource": { "type": "Car" }, "action": { "name": "CAN_BUY" }, "context": { "input_params": { "max_price": 5000 } } } ``` Response - only the Skoda Octavia fits the budget. **Response 200** ```json { "results": [ { "type": "Car", "id": "skodaOctavia" } ] } ``` Same resource search in Python. **search_resource.py** ```python import http.client import json conn = http.client.HTTPSConnection("eu.api.indykite.com") payload = json.dumps({ "subject": {"type": "Person", "id": "knightrider"}, "resource": {"type": "Car"}, "action": {"name": "CAN_BUY"}, "context": {"input_params": {"max_price": 5000}} }) headers = { 'Content-Type': "application/json", 'X-IK-ClientKey': "" } conn.request("POST", "/access/v1/search/resource", payload, headers) res = conn.getresponse() data = res.read() ``` ### Step 5 Subject search: who can buy the Skoda Octavia under 5,000? **POST https://eu.api.indykite.com/access/v1/search/subject** ```json { "subject": { "type": "Person" }, "resource": { "type": "Car", "id": "skodaOctavia" }, "action": { "name": "CAN_BUY" }, "context": { "input_params": { "max_price": 5000 } } } ``` Response - Persons who can afford it and don't already own it. **Response 200** ```json { "results": [ { "type": "Person", "id": "knightrider" }, { "type": "Person", "id": "karel" } ] } ``` ## Common Errors ### 400: missing input parameter: max_price **Solution:** Send context.input_params.max_price on every request. The partial filter cannot resolve without it. ### 401: UNAUTHENTICATED **Solution:** Check X-IK-ClientKey contains valid AppAgent credentials. --- Source: https://developer.indykite.com/resources/authz-4 --- # KBAC: Check a Permission Grid for One Subject with authZEN Evaluations (Boxcar) > Use the authZEN /access/v1/evaluations endpoint to check, in a single call, whether one subject is allowed across a grid of (resource, action) cells. The response returns one decision per cell, in order; the client keeps the cells whose decision is true. **Category:** KBAC **API:** KBAC **Tags:** KBAC Policy, authZEN, Evaluations, Boxcarring, Batch Decision, Cross-Action, Discovery **Last Updated:** 2026-07-14 **OpenAPI Endpoints:** /configs/v1/authorization-policies, /access/v1/evaluations **Related Guides:** /guides/guide-dynamic-authz, /guides/guide-authzen ## Summary This example demonstrates the authZEN /access/v1/evaluations (boxcarring) endpoint to discover what one subject can do across several (resource, action) cells in a single round-trip. Note: the older /access/v1/what-authorized endpoint is deprecated. Use /access/v1/evaluations (this example) for grid checks, or the search endpoints for type-level discovery. How the evaluations endpoint works: - Top-level subject / action / resource act as defaults. - Each entry in the evaluations array can override any of them; missing fields inherit the defaults. - The response is an evaluations array with one { decision: true|false } per request, in the same order. How it relates to the search endpoints: - /search/action - "for THIS subject + THIS resource, which actions are allowed?" - /search/resource - "for THIS subject + THIS action, which resources are allowed?" - /search/subject - "for THIS resource + THIS action, which subjects are allowed?" - /evaluations - "for THIS subject, decide each of these concrete (resource, action) cells at once." Use /evaluations when you already know the concrete resources to check. For type-level discovery ("which cars can the subject drive?") use /search/resource (see authz-1 and authz-4). Policy version note: the policies in this example use "policy_version": "2.0-kbac". The same policy JSON is also accepted with "policy_version": "3.0-kbac" (identical schema, raw-Cypher semantics; do not reference $subject_id or external properties). Only 3.0-kbac additionally accepts USE graph.byName() routing and CALL { } subqueries for composite / data-residency IKGs - see resources authz-7 and authz-8. ## Use Case Scenario: A self-service portal renders a side menu. Each item maps to a concrete (resource, action) cell. Before rendering, the portal asks IndyKite which items are available to the signed-in user in one boxcar call. Default subject: Karel Candidate cells: - (Car kitt, CAN_DRIVE) - (Bus harmonika, CAN_RIDE) - (Car kitt, CAN_WASH) Policies in scope: - Person can CAN_DRIVE a Car if a DRIVES relationship exists. - Person can CAN_RIDE a Bus if they hold a Ticket for it (Person -HAS-> Ticket -FOR-> Bus). - (No CAN_WASH policy defined.) Karel holds a Ticket (listek) that is FOR the Harmonika bus, but does not drive kitt and no CAN_WASH policy applies. The evaluations response is therefore [false, true, false] - one decision per cell, in order. Render only the menu item whose decision is true (CAN_RIDE the bus); leave the others hidden or grayed out. ## Requirements Prerequisites: - ServiceAccount credentials: For creating the policies. - AppAgent credentials: For calling /access/v1/evaluations (X-IK-ClientKey). - Graph data including Person, Car, Bus, and Ticket nodes, with DRIVES relationships and a Person -HAS-> Ticket -FOR-> Bus path. - At least one policy per (resource, action) cell you want to evaluate. Required API access: - POST /configs/v1/authorization-policies (one per action) - POST /access/v1/evaluations ## Steps Step 1: Capture Graph Data - Action: POST Person, Car, Bus, Ticket nodes; DRIVES relationships and the Person -HAS-> Ticket -FOR-> Bus path. - Result: Karel -HAS-> Ticket(listek) -FOR-> Bus(harmonika); no DRIVES from Karel. Step 2: Create Per-Action Policies - Action: POST one policy for CAN_DRIVE on Car, one for CAN_RIDE on Bus. Leave CAN_WASH undefined intentionally. - Result: Policy IDs returned. Step 3: Call /access/v1/evaluations - Authentication: AppAgent credential. - Action: POST a default subject (Karel) plus an evaluations array of concrete (resource, action) cells. - Result: An evaluations array with one decision per cell, in order. Keep the cells where decision is true. ## Code Examples ### Step 1 Capture nodes used in this example. Re-use the authZEN dataset. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "alice", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "alice@email.com" }, { "type": "given_name", "value": "Alice" }, { "type": "last_name", "value": "Smith" } ] }, { "external_id": "knightrider", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "knightrider@demo.com" }, { "type": "name", "value": "Michael Knight" } ] }, { "external_id": "satchmo", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "satchmo@demo.com" }, { "type": "name", "value": "Louis Armstrong" } ] }, { "external_id": "karel", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "karel@demo.com" }, { "type": "name", "value": "Karel Plihal" } ] }, { "external_id": "kitt", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "pontiac" }, { "type": "model", "value": "Firebird" } ] }, { "external_id": "cadillacv16", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "Cadillac" }, { "type": "model", "value": "V-16" } ] }, { "external_id": "harmonika", "type": "Bus", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "Ikarus" }, { "type": "model", "value": "280" } ] }, { "external_id": "listek", "type": "Ticket", "is_identity": false }, { "external_id": "airbook-xyz", "type": "Laptop", "is_identity": false } ] } ``` Capture DRIVES relationships plus the Person -HAS-> Ticket -FOR-> Bus path (karel HAS listek, listek FOR harmonika). **POST https://eu.api.indykite.com/capture/v1/relationships/** ```json { "relationships": [ { "source": { "external_id": "knightrider", "type": "Person" }, "target": { "external_id": "kitt", "type": "Car" }, "type": "DRIVES" }, { "source": { "external_id": "satchmo", "type": "Person" }, "target": { "external_id": "cadillacv16", "type": "Car" }, "type": "DRIVES" }, { "source": { "external_id": "karel", "type": "Person" }, "target": { "external_id": "listek", "type": "Ticket" }, "type": "HAS" }, { "source": { "external_id": "listek", "type": "Ticket" }, "target": { "external_id": "harmonika", "type": "Bus" }, "type": "FOR" }, { "source": { "external_id": "karel", "type": "Person" }, "target": { "external_id": "airbook-xyz", "type": "Laptop" }, "type": "OWNS" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "airbook-xyz", "type": "Laptop" }, "type": "OWNS" }, { "source": { "external_id": "knightrider", "type": "Person" }, "target": { "external_id": "kitt", "type": "Car" }, "type": "OWNS" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "cadillacv16", "type": "Car" }, "type": "DRIVES" } ] } ``` ### Step 2 CAN_DRIVE policy (Person -[DRIVES]-> Car). **policy_drive.json** ```json { "meta": { "policy_version": "2.0-kbac" }, "subject": { "type": "Person" }, "actions": [ "CAN_DRIVE" ], "resource": { "type": "Car" }, "condition": { "cypher": "MATCH (subject:Person)-[:DRIVES]->(resource:Car)" } } ``` Request to create the CAN_DRIVE policy. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"meta\":{\"policy_version\":\"2.0-kbac\"},\"subject\":{\"type\":\"Person\"},\"actions\":[\"CAN_DRIVE\"],\"resource\":{\"type\":\"Car\"},\"condition\":{\"cypher\":\"MATCH (subject:Person)-[:DRIVES]->(resource:Car)\"}}", "status": "ACTIVE", "tags": [] } ``` CAN_RIDE policy (Person -HAS-> Ticket -FOR-> Bus). **policy_ride.json** ```json { "meta": { "policy_version": "2.0-kbac" }, "subject": { "type": "Person" }, "actions": [ "CAN_RIDE" ], "resource": { "type": "Bus" }, "condition": { "cypher": "MATCH (subject:Person)-[:HAS]->(ticket:Ticket)-[:FOR]->(resource:Bus)" } } ``` Request to create the CAN_RIDE policy. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "Authorize CAN_RIDE on Bus when the subject holds a Ticket for it (Person -HAS-> Ticket -FOR-> Bus).", "display_name": "policy - person can ride a bus", "name": "policy-person-can-ride-a-bus", "policy": "{\"meta\":{\"policy_version\":\"2.0-kbac\"},\"subject\":{\"type\":\"Person\"},\"actions\":[\"CAN_RIDE\"],\"resource\":{\"type\":\"Bus\"},\"condition\":{\"cypher\":\"MATCH (subject:Person)-[:HAS]->(ticket:Ticket)-[:FOR]->(resource:Bus)\"}}", "status": "ACTIVE", "tags": [] } ``` ### Step 3 Boxcar evaluations: a default subject (Karel) plus three concrete (resource, action) cells to decide at once. **POST https://eu.api.indykite.com/access/v1/evaluations** ```json { "subject": { "type": "Person", "id": "karel" }, "evaluations": [ { "resource": { "type": "Car", "id": "kitt" }, "action": { "name": "CAN_DRIVE" } }, { "resource": { "type": "Bus", "id": "harmonika" }, "action": { "name": "CAN_RIDE" } }, { "resource": { "type": "Car", "id": "kitt" }, "action": { "name": "CAN_WASH" } } ] } ``` Response - one decision per cell, in request order: [CAN_DRIVE kitt = false, CAN_RIDE harmonika = true, CAN_WASH kitt = false]. **Response 200** ```json { "evaluations": [ { "decision": false }, { "decision": true }, { "decision": false } ] } ``` Same call in Python. **evaluations.py** ```python import http.client import json conn = http.client.HTTPSConnection("eu.api.indykite.com") payload = json.dumps({ "subject": {"type": "Person", "id": "karel"}, "evaluations": [ {"resource": {"type": "Car", "id": "kitt"}, "action": {"name": "CAN_DRIVE"}}, {"resource": {"type": "Bus", "id": "harmonika"}, "action": {"name": "CAN_RIDE"}}, {"resource": {"type": "Car", "id": "kitt"}, "action": {"name": "CAN_WASH"}} ] }) headers = { 'Content-Type': "application/json", 'X-IK-ClientKey': "" } conn.request("POST", "/access/v1/evaluations", payload, headers) res = conn.getresponse() data = res.read() ``` ## Common Errors ### 400: evaluations must be a non-empty array **Solution:** Send at least one entry in evaluations. Each entry resolves resource and action from the top-level defaults unless it overrides them. ### 200: every decision is false unexpectedly **Solution:** Confirm a policy exists for each (resource, action) cell and the subject has the required graph relationship (e.g. the Ticket -FOR-> Bus path for CAN_RIDE). Decisions are returned in the same order as the request. --- Source: https://developer.indykite.com/resources/authz-5 --- # KBAC: Token-Scope-Gated Read/Write Permissions > Gate KBAC actions on the bearer token's scope claim. CAN_READ requires scope 'cars.read', CAN_WRITE requires scope 'cars.write'. The graph relationship grants the base capability; the token scope decides which verbs are exercisable on this request. **Category:** KBAC **API:** KBAC **Tags:** KBAC Policy, authZEN, Token Scope, $token.scope, Token Filter, Scoped Access **Last Updated:** 2026-07-14 **OpenAPI Endpoints:** /configs/v1/authorization-policies, /configs/v1/token-introspects, /access/v1/evaluation **Related Guides:** /guides/guide-dynamic-authz, /guides/guide-authzen, /guides/guide-token-introspect ## Summary This example demonstrates KBAC policies that take both the graph and the bearer token's scope claim into account. Pattern: - The condition.cypher establishes the relationship-based base permission (e.g., subject DRIVES resource). - The condition.filter narrows the action by inspecting $token.scope with the CONTAINS operator: - CAN_READ requires the scope claim to contain 'cars.read'. - CAN_WRITE requires the scope claim to contain 'cars.write'. - Two separate policies coexist; the runtime selects them by action. Effect: a single user identity (Knight Rider, who DRIVES KITT) can be authorized for CAN_READ today but denied CAN_WRITE because the issued bearer token does not include the cars.write scope. Re-authenticate with a wider-scoped token to gain CAN_WRITE without changing any policy. Policy version note: the policies in this example use "policy_version": "2.0-kbac". The same policy JSON is also accepted with "policy_version": "3.0-kbac" (identical schema, raw-Cypher semantics; do not reference $subject_id or external properties). Only 3.0-kbac additionally accepts USE graph.byName() routing and CALL { } subqueries for composite / data-residency IKGs - see resources authz-7 and authz-8. Note for 3.0-kbac with bearer tokens: the token's subject must match the requested subject; mismatches are denied with 403 Forbidden. ## Use Case Scenario: A vehicle service portal issues bearer tokens with scopes that match what the user can actually do this session. - Mechanic token: scope = "cars.read cars.write" -> can read AND modify car records. - Customer token: scope = "cars.read" -> can read their own car record, cannot modify it. Policies: 1. CAN_READ on Car when subject DRIVES car AND $token.scope contains 'cars.read'. 2. CAN_WRITE on Car when subject DRIVES car AND $token.scope contains 'cars.write'. Calls: - Knight Rider with cars.read token: CAN_READ KITT -> true; CAN_WRITE KITT -> false. - Mechanic with full scopes: both -> true. This is the cleanest place to put scope checks: in the policy condition, not in the client. ## Requirements Prerequisites: - ServiceAccount credentials: For creating the policies. - AppAgent credentials: For calling /access/v1/evaluation. - A Token Introspect configuration so the API can verify the bearer token and extract its scope claim. - Graph data with DRIVES relationships. Required API access: - POST /configs/v1/authorization-policies (one per action) - POST /access/v1/evaluation (caller passes the user's bearer token in the Authorization header) ## Steps Step 1: Capture Graph Data - Action: POST Person and Car nodes plus DRIVES relationships (re-use the authZEN dataset). Step 2: Create the CAN_READ Policy - Action: POST a policy whose condition.filter requires $token.scope to CONTAIN 'cars.read'. Step 3: Create the CAN_WRITE Policy - Action: POST a similar policy requiring $token.scope to CONTAIN 'cars.write'. Step 4: Evaluate with a Narrow-Scope Token - Action: POST /access/v1/evaluation with action CAN_READ, sending Authorization: Bearer where the token's scope claim contains only cars.read. - Result: authorized. Step 5: Evaluate the Same Subject for CAN_WRITE - Action: POST /access/v1/evaluation with action CAN_WRITE using the same narrow-scope token. - Result: denied - the scope check fails even though DRIVES exists. ## Code Examples ### Step 1 Capture Person and Car nodes. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "alice", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "alice@email.com" }, { "type": "given_name", "value": "Alice" }, { "type": "last_name", "value": "Smith" } ] }, { "external_id": "knightrider", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "knightrider@demo.com" }, { "type": "name", "value": "Michael Knight" } ] }, { "external_id": "satchmo", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "satchmo@demo.com" }, { "type": "name", "value": "Louis Armstrong" } ] }, { "external_id": "karel", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "karel@demo.com" }, { "type": "name", "value": "Karel Plihal" } ] }, { "external_id": "kitt", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "pontiac" }, { "type": "model", "value": "Firebird" } ] }, { "external_id": "cadillacv16", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "Cadillac" }, { "type": "model", "value": "V-16" } ] }, { "external_id": "harmonika", "type": "Bus", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "Ikarus" }, { "type": "model", "value": "280" } ] }, { "external_id": "listek", "type": "Ticket", "is_identity": false }, { "external_id": "airbook-xyz", "type": "Laptop", "is_identity": false } ] } ``` Capture DRIVES relationships. **POST https://eu.api.indykite.com/capture/v1/relationships/** ```json { "relationships": [ { "source": { "external_id": "knightrider", "type": "Person" }, "target": { "external_id": "kitt", "type": "Car" }, "type": "DRIVES" }, { "source": { "external_id": "satchmo", "type": "Person" }, "target": { "external_id": "cadillacv16", "type": "Car" }, "type": "DRIVES" }, { "source": { "external_id": "karel", "type": "Person" }, "target": { "external_id": "listek", "type": "Ticket" }, "type": "HAS" }, { "source": { "external_id": "listek", "type": "Ticket" }, "target": { "external_id": "harmonika", "type": "Bus" }, "type": "FOR" }, { "source": { "external_id": "karel", "type": "Person" }, "target": { "external_id": "airbook-xyz", "type": "Laptop" }, "type": "OWNS" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "airbook-xyz", "type": "Laptop" }, "type": "OWNS" }, { "source": { "external_id": "knightrider", "type": "Person" }, "target": { "external_id": "kitt", "type": "Car" }, "type": "OWNS" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "cadillacv16", "type": "Car" }, "type": "DRIVES" } ] } ``` ### Step 2 Policy: CAN_READ on Car requires the bearer token's scope claim to contain 'cars.read'. **policy_read.json** ```json { "meta": { "policy_version": "2.0-kbac" }, "subject": { "type": "Person" }, "actions": [ "CAN_READ" ], "resource": { "type": "Car" }, "condition": { "cypher": "MATCH (subject:Person)-[:DRIVES]->(resource:Car)", "filter": { "operator": "CONTAINS", "attribute": "$token.scope", "value": "cars.read" } } } ``` Request to create the CAN_READ policy. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "Authorize CAN_READ on Car only when the access token's scope claim contains 'cars.read'.", "display_name": "policy - scope cars.read", "name": "policy-scope-cars-read", "policy": "{\"meta\":{\"policy_version\":\"2.0-kbac\"},\"subject\":{\"type\":\"Person\"},\"actions\":[\"CAN_READ\"],\"resource\":{\"type\":\"Car\"},\"condition\":{\"cypher\":\"MATCH (subject:Person)-[:DRIVES]->(resource:Car)\",\"filter\":{\"operator\":\"CONTAINS\",\"attribute\":\"$token.scope\",\"value\":\"cars.read\"}}}", "status": "ACTIVE", "tags": [] } ``` ### Step 3 Policy: CAN_WRITE on Car requires the bearer token's scope claim to contain 'cars.write'. **policy_write.json** ```json { "meta": { "policy_version": "2.0-kbac" }, "subject": { "type": "Person" }, "actions": [ "CAN_WRITE" ], "resource": { "type": "Car" }, "condition": { "cypher": "MATCH (subject:Person)-[:DRIVES]->(resource:Car)", "filter": { "operator": "CONTAINS", "attribute": "$token.scope", "value": "cars.write" } } } ``` Request to create the CAN_WRITE policy. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "Authorize CAN_WRITE on Car only when the access token's scope claim contains 'cars.write'.", "display_name": "policy - scope cars.write", "name": "policy-scope-cars-write", "policy": "{\"meta\":{\"policy_version\":\"2.0-kbac\"},\"subject\":{\"type\":\"Person\"},\"actions\":[\"CAN_WRITE\"],\"resource\":{\"type\":\"Car\"},\"condition\":{\"cypher\":\"MATCH (subject:Person)-[:DRIVES]->(resource:Car)\",\"filter\":{\"operator\":\"CONTAINS\",\"attribute\":\"$token.scope\",\"value\":\"cars.write\"}}}", "status": "ACTIVE", "tags": [] } ``` ### Step 4 Evaluation: Knight Rider asks if he can CAN_READ KITT. Send Authorization: Bearer where the token's scope claim contains cars.read (plus X-IK-ClientKey for the AppAgent). **POST https://eu.api.indykite.com/access/v1/evaluation** ```json { "subject": { "type": "Person", "id": "knightrider" }, "resource": { "type": "Car", "id": "kitt" }, "action": { "name": "CAN_READ" } } ``` Response - authorized (DRIVES exists and the token scope contains cars.read). **Response 200** ```json { "decision": true } ``` ### Step 5 Same subject and resource, but action is CAN_WRITE and the same token only carries cars.read. The condition.filter scope check denies. **POST https://eu.api.indykite.com/access/v1/evaluation** ```json { "subject": { "type": "Person", "id": "knightrider" }, "resource": { "type": "Car", "id": "kitt" }, "action": { "name": "CAN_WRITE" } } ``` Response - denied. The DRIVES relationship is present but the scope check fails. **Response 200** ```json { "decision": false } ``` Same CAN_READ evaluation in Python (token passed in the Authorization header). **evaluate_scope_read.py** ```python import http.client import json conn = http.client.HTTPSConnection("eu.api.indykite.com") payload = json.dumps({ "subject": {"type": "Person", "id": "knightrider"}, "resource": {"type": "Car", "id": "kitt"}, "action": {"name": "CAN_READ"} }) headers = { 'Content-Type': "application/json", 'X-IK-ClientKey': "", # User access token whose scope claim must contain 'cars.read' 'Authorization': "Bearer " } conn.request("POST", "/access/v1/evaluation", payload, headers) res = conn.getresponse() data = res.read() ``` ## Common Errors ### 401: invalid bearer token **Solution:** Confirm the Token Introspect configuration matches the issuer of the bearer token. The scope claim cannot be evaluated against an unverified token. ### 200: decision: false unexpectedly **Solution:** Print the introspected token to confirm the scope claim is present and includes the expected value. The token filter does an IN check against the list of space-separated scopes. --- Source: https://developer.indykite.com/resources/authz-6 --- # KBAC 3.0: Raw-Cypher Policies with CALL { } Subqueries and USE Routing > Author 3.0-kbac policies using the Cypher keywords that only this version accepts: USE graph.byName() database routing and CALL { } subqueries with inner RETURNs. The condition runs as raw Cypher - the platform only pins the subject/resource and appends the projection; the USE routing shown here targets a composite IKG. **Category:** KBAC **API:** KBAC **Tags:** KBAC Policy, 3.0-kbac, Raw Cypher, USE graph.byName, CALL Subquery, Composite Database, authZEN **Last Updated:** 2026-07-14 **OpenAPI Endpoints:** /configs/v1/authorization-policies, /access/v1/evaluation **Related Guides:** /guides/guide-data-residency, /guides/guide-authzen, /guides/guide-dynamic-authz ## Summary This example is 3.0-kbac ONLY - every policy shown here is rejected on 2.0-kbac. What 3.0-kbac changes: A 2.0-kbac condition is rewritten by the platform and always runs against the default database. A 3.0-kbac condition is RAW Cypher: it runs exactly as authored, and you write the composite-database routing yourself. Newly accepted Cypher keywords (rejected on 2.0-kbac, legal on 3.0-kbac): 1. USE graph.byName(...) - routes the match to a constituent database of the composite IKG. The argument must be a string literal or a single parameter; expressions are rejected at creation. 2. CALL { } subqueries - each subquery can carry its own USE clause, so one condition can combine matches from several constituents. The examples write them as CALL () { ... }, the explicit variable-scope form: the parentheses list the outer variables imported into the subquery (empty = import none). 3. RETURN inside a CALL { } subquery - required to hand rows back to the enclosing query. A top-level RETURN is still rejected: the platform appends the projection itself. Still blocked in both versions: mutating clauses (CREATE, DELETE, DETACH, DROP, FOREACH, MERGE, REMOVE, SET). Authoring rules specific to 3.0-kbac: - The condition must still bind the variables subject and resource. - $subject_id must not be referenced (composite node IDs are not stable across locations); rejected at creation. - External (resolver-backed) properties must not be referenced; rejected at creation ("external properties cannot be used in data-residency policies"). - $subject_external_id, $subject_type, $resource_external_id, $resource_type are bound by the platform - never supplied via input_params. - The subject does not need is_identity: 3.0-kbac matches it by type + external ID (2.0-kbac requires an identity-node subject). With a bearer token, the token's subject must match the requested subject (403 Forbidden otherwise). - Schema is otherwise identical to 2.0-kbac: any valid 2.0 condition is also a valid 3.0 condition (unless it references $subject_id or external properties). Note: 3.0-kbac itself does not require a composite database - only USE routing does. The policies in this example route, so they need one; a 3.0-kbac policy without USE evaluates against the default database as plain raw Cypher. For dynamic, per-request location routing with USE graph.byName($param), see authz-8. ## Use Case Scenario: A car-sharing platform runs a composite IKG (see the data residency guide): the default/global constituent holds proxy nodes and cross-location relationships, while full node data lives in per-location constituents such as ikcomposite.db2. The authorization team wants decisions evaluated against a location constituent - not just the global proxies - because that is where the full nodes live: Policy 1 (static USE): the whole condition runs inside ikcomposite.db2. "Can alice drive KITT?" is answered from the east database's full data. Policy 2 (CALL subquery): the same routing expressed as a CALL { } subquery with an inner RETURN - the shape to build on when a condition needs to combine matches from more than one constituent (each subquery can carry its own USE clause). Both policies answer the same AuthZEN calls as any other KBAC policy: nothing changes for the calling application. ## Requirements Prerequisites: - A customer-hosted composite-database project (composite_db_name + alias_mapping configured; see the data residency guide and resource residency-1) - Nodes ingested with locations, e.g. Person(person-alice) and Car(car-kitt) with an OWNS relationship in the east constituent (ikcomposite.db2) - ServiceAccount credentials: For creating policies (Bearer token) - AppAgent credentials: For authorization queries (X-IK-ClientKey) Required API access: - POST /configs/v1/authorization-policies (create policy) - POST /access/v1/evaluation (authorization check) Version requirement: meta.policy_version must be "3.0-kbac". The same Cypher fails on 2.0-kbac with "USE clause is not allowed" / "Cypher contains forbidden clauses: [CALL]". ## Steps Step 1: Create a 3.0-kbac policy with a static USE clause - Authentication: ServiceAccount credential (Bearer token) - Action: POST the policy; the condition starts with USE graph.byName('ikcomposite.db2') - Result: The Cypher is validated raw (EXPLAINed against the database, not rewritten); policy ID returned Step 2: Create a 3.0-kbac policy with a CALL { } subquery - Authentication: ServiceAccount credential (Bearer token) - Action: POST the policy; the condition wraps the match in CALL () { USE ... MATCH ... RETURN subject, resource } - Result: CALL, per-subquery USE, and the inner RETURN are all accepted - each would be rejected on 2.0-kbac Step 3: Evaluate - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: POST to /access/v1/evaluation with subject, action, resource - the request shape is unchanged - Result: {decision: true} evaluated inside the routed constituent Step 4: Cleanup - Action: DELETE the policy configurations ## Code Examples ### Step 1 3.0-kbac policy with a static USE clause. The condition is raw Cypher: the platform keeps it verbatim, pins subject/resource by type + external_id, and replaces the projection. On 2.0-kbac this exact Cypher fails with 'USE clause is not allowed'. **policy_static_use.json** ```json { "meta": { "policy_version": "3.0-kbac" }, "subject": { "type": "Person" }, "actions": [ "CAN_DRIVE" ], "resource": { "type": "Car" }, "condition": { "cypher": "USE graph.byName('ikcomposite.db2') MATCH (subject:Person)-[:OWNS]->(resource:Car)" } } ``` Request to create the static-USE policy using REST. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "3.0-kbac raw-Cypher policy. The condition is executed as authored: the USE graph.byName() clause pins the whole match to the ikcomposite.db2 constituent database. Rejected on 2.0-kbac (USE clause is not allowed).", "display_name": "policy - can drive (static USE routing)", "name": "policy-can-drive-static-use", "policy": "{\"meta\":{\"policy_version\":\"3.0-kbac\"},\"subject\":{\"type\":\"Person\"},\"actions\":[\"CAN_DRIVE\"],\"resource\":{\"type\":\"Car\"},\"condition\":{\"cypher\":\"USE graph.byName('ikcomposite.db2') MATCH (subject:Person)-[:OWNS]->(resource:Car)\"}}", "status": "ACTIVE", "tags": [] } ``` ### Step 2 3.0-kbac policy using a CALL { } subquery with its own USE clause and an inner RETURN - all three newly accepted keywords in one condition. On 2.0-kbac the CALL keyword alone fails with 'Cypher contains forbidden clauses: [CALL]'. Note the subquery RETURNs subject and resource so the platform can pin them. **policy_call_subquery.json** ```json { "meta": { "policy_version": "3.0-kbac" }, "subject": { "type": "Person" }, "actions": [ "CAN_DRIVE" ], "resource": { "type": "Car" }, "condition": { "cypher": "CALL () { USE graph.byName('ikcomposite.db2') MATCH (subject:Person)-[:OWNS]->(resource:Car) RETURN subject, resource }" } } ``` Request to create the CALL-subquery policy using REST. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "3.0-kbac policy demonstrating the newly accepted keywords: a CALL { } subquery with a per-subquery USE graph.byName() clause and an inner RETURN. The example is written CALL () { ... } - the explicit variable-scope form, where the parentheses list the outer variables imported into the subquery (empty = none). Each of the three (CALL, USE, inner RETURN) is rejected on 2.0-kbac; on 3.0-kbac they are legal composite-database routing. Mutating clauses (CREATE, MERGE, SET, DELETE, ...) and a top-level RETURN remain blocked.", "display_name": "policy - can drive (CALL subquery routing)", "name": "policy-can-drive-call-subquery", "policy": "{\"meta\":{\"policy_version\":\"3.0-kbac\"},\"subject\":{\"type\":\"Person\"},\"actions\":[\"CAN_DRIVE\"],\"resource\":{\"type\":\"Car\"},\"condition\":{\"cypher\":\"CALL () { USE graph.byName('ikcomposite.db2') MATCH (subject:Person)-[:OWNS]->(resource:Car) RETURN subject, resource }\"}}", "status": "ACTIVE", "tags": [] } ``` ### Step 3 AuthZEN evaluation - the request shape is identical to any other KBAC evaluation; the routing lives entirely in the policy. **POST https://eu.api.indykite.com/access/v1/evaluation** ```json { "subject": { "type": "Person", "id": "person-alice" }, "resource": { "type": "Car", "id": "car-kitt" }, "action": { "name": "CAN_DRIVE" } } ``` Response - the decision was evaluated inside the ikcomposite.db2 constituent. **Response 200** ```json { "decision": true } ``` ### Step 4 Delete the policies when done. **DELETE https://eu.api.indykite.com/configs/v1/authorization-policies/{id}** ```json { "id": "your_policy_configuration_gid" } ``` ## Common Errors ### 422: USE clause is not allowed / Cypher contains forbidden clauses: [CALL] **Solution:** The policy was created with policy_version 2.0-kbac. USE and CALL { } require meta.policy_version "3.0-kbac" ### 422: graph.byName() argument must be a string literal or a single parameter **Solution:** Do not compute the routing target (concatenation, coalesce(...), etc.). Use 'composite.alias' as a literal, or a single $parameter (see authz-8) ### 422: parameter "$subject_id" is reserved and cannot be referenced **Solution:** 3.0-kbac identifies subjects by type and external ID only - remove the $subject_id reference; the subject is already pinned automatically ### 422: Cypher parse error near RETURN **Solution:** A top-level RETURN is still rejected on 3.0-kbac - the platform appends the projection. RETURN is only legal inside a CALL { } subquery --- Source: https://developer.indykite.com/resources/authz-7 --- # KBAC 3.0: Data Residency - Location-Routed AuthZEN Decisions > Route authorization decisions to a specific location of a composite IKG at request time. A 3.0-kbac policy declares USE graph.byName($region); each AuthZEN call supplies a logical location (an alias_mapping key) in context.input_params, and IndyKite translates it to the physical constituent database. **Category:** KBAC **API:** KBAC **Tags:** KBAC Policy, 3.0-kbac, Data Residency, Location Parameter, Composite Database, authZEN, Input Params **Last Updated:** 2026-07-14 **OpenAPI Endpoints:** /configs/v1/authorization-policies, /access/v1/evaluation, /access/v1/search/resource **Related Guides:** /guides/guide-data-residency, /guides/guide-authzen, /guides/guide-dynamic-authz ## Summary This example is 3.0-kbac ONLY - it makes AuthZEN decisions location-aware on a composite IKG. How it works: 1. The policy condition starts with USE graph.byName($region). Because $region routes graph.byName(), it becomes a LOCATION PARAMETER. 2. Each AuthZEN request supplies a logical location in context.input_params (e.g. {"region": "east"}). Logical locations are the KEYS of the project's alias_mapping (global=db1&east=db2&west=db3) - callers never see or send physical database names. 3. Just before execution, IndyKite translates the logical location to the physical constituent (e.g. ikcomposite.db2) and runs the raw condition there. The same request works on every AuthZEN endpoint: /access/v1/evaluation, /access/v1/evaluations (put the location in the default or per-evaluation context), and the three search endpoints. Location parameter rules: - A location parameter must not be referenced anywhere else in the Cypher (its value is rewritten to the physical alias at request time). - It must arrive as a non-empty string, and must be a key of alias_mapping - otherwise the call fails with 422 Unprocessable Entity. - Policies without any graph.byName($param) simply have no location parameters - nothing changes for them. Residency is opt-in per policy: 2.0-kbac policies keep running against the default database, untouched. 3.0-kbac itself does not require a composite database either - only USE routing does; a 3.0-kbac policy without USE evaluates against the default database as plain raw Cypher. ## Use Case Scenario: A car-sharing platform stores each jurisdiction's personal data in its own constituent database (east, west), as set up in resource residency-1. The mobile app knows which jurisdiction the signed-in user belongs to and asks for decisions in that location: - "Can alice drive KITT?" with region=east -> true: alice's full data and her OWNS relationship live in the east constituent. - The same question with region=west -> false: the west constituent has no such data. - region=north -> 422 Unprocessable Entity: north is not a key of the project's alias_mapping. - "Which cars can alice drive in east?" (search/resource with region=east) -> [car-kitt]. One policy serves every location; the caller picks the location per request. ## Requirements Prerequisites: - A customer-hosted composite-database project with alias_mapping, e.g. global=db1&east=db2&west=db3 (see resource residency-1 for the full setup) - Nodes ingested with locations: Person(person-alice) and Car(car-kitt) with an OWNS relationship in the east location - ServiceAccount credentials: For creating policies (Bearer token) - AppAgent credentials: For authorization queries (X-IK-ClientKey) Required API access: - POST /configs/v1/authorization-policies (create policy) - POST /access/v1/evaluation (single check) - POST /access/v1/search/resource (which resources, in this location) Version requirement: meta.policy_version must be "3.0-kbac" - location parameters do not exist on 2.0-kbac. ## Steps Step 1: Create the location-routed 3.0-kbac policy - Authentication: ServiceAccount credential (Bearer token) - Action: POST the policy whose condition starts with USE graph.byName($region) - Result: $region is registered as a location parameter of the policy Step 2: Evaluate in the east location - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: POST to /access/v1/evaluation with context.input_params.region = "east" - Result: {decision: true} - evaluated inside the constituent mapped to east Step 3: Evaluate the same question in the west location - Action: Same request with region = "west" - Result: {decision: false} - the west constituent holds no matching data Step 4: See what happens with an unmapped location - Action: Same request with region = "north" (not a key of alias_mapping) - Result: 422 Unprocessable Entity - unknown location "north" for parameter "$region" Step 5: Search resources within a location - Action: POST to /access/v1/search/resource with region = "east" - Result: The list of cars alice can drive according to the east constituent Step 6: Cleanup - Action: DELETE the policy configuration ## Code Examples ### Step 1 3.0-kbac policy with dynamic routing. $region routes graph.byName() and must not be referenced anywhere else in the Cypher. Platform-bound parameters ($subject_external_id, $subject_type, $resource_external_id, $resource_type) cannot be used as routing parameters, and $subject_id must not appear at all. **policy_location_routed.json** ```json { "meta": { "policy_version": "3.0-kbac" }, "subject": { "type": "Person" }, "actions": [ "CAN_DRIVE" ], "resource": { "type": "Car" }, "condition": { "cypher": "USE graph.byName($region) MATCH (subject:Person)-[:OWNS]->(resource:Car)" } } ``` Request to create the location-routed policy using REST. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "3.0-kbac data-residency policy. $region is a location parameter: it routes graph.byName() and must not be referenced anywhere else in the Cypher. Callers supply a logical location (an alias_mapping key such as east or west) in context.input_params.region; IndyKite translates it to the physical constituent database just before execution.", "display_name": "policy - can drive (location-routed)", "name": "policy-can-drive-location-routed", "policy": "{\"meta\":{\"policy_version\":\"3.0-kbac\"},\"subject\":{\"type\":\"Person\"},\"actions\":[\"CAN_DRIVE\"],\"resource\":{\"type\":\"Car\"},\"condition\":{\"cypher\":\"USE graph.byName($region) MATCH (subject:Person)-[:OWNS]->(resource:Car)\"}}", "status": "ACTIVE", "tags": [] } ``` ### Step 2 Evaluation routed to the east location. "east" is a logical location - a key of the project's alias_mapping - not a Neo4j database name. IndyKite resolves it to the physical constituent just before running the condition. **POST https://eu.api.indykite.com/access/v1/evaluation** ```json { "subject": { "type": "Person", "id": "person-alice" }, "resource": { "type": "Car", "id": "car-kitt" }, "action": { "name": "CAN_DRIVE" }, "context": { "input_params": { "region": "east" } } } ``` Response - allowed: the OWNS relationship exists in the east constituent. **Response 200** ```json { "decision": true } ``` The same east-routed evaluation in Python. **evaluation_east.py** ```python import http.client import json conn = http.client.HTTPSConnection("eu.api.indykite.com") payload = json.dumps({ "subject": {"type": "Person", "id": "person-alice"}, "resource": {"type": "Car", "id": "car-kitt"}, "action": {"name": "CAN_DRIVE"}, # "east" is a logical location (an alias_mapping key), never a Neo4j database name "context": {"input_params": {"region": "east"}} }) headers = { 'Content-Type': "application/json", 'X-IK-ClientKey': "" } conn.request("POST", "/access/v1/evaluation", payload, headers) res = conn.getresponse() data = res.read() ``` ### Step 3 The identical question routed to the west location. **POST https://eu.api.indykite.com/access/v1/evaluation** ```json { "subject": { "type": "Person", "id": "person-alice" }, "resource": { "type": "Car", "id": "car-kitt" }, "action": { "name": "CAN_DRIVE" }, "context": { "input_params": { "region": "west" } } } ``` Response - denied: no matching data in the west constituent. Same subject, same resource, same policy; only the location changed. **Response 200** ```json { "decision": false } ``` ### Step 4 Request with a location that is not a key of alias_mapping. **POST https://eu.api.indykite.com/access/v1/evaluation** ```json { "subject": { "type": "Person", "id": "person-alice" }, "resource": { "type": "Car", "id": "car-kitt" }, "action": { "name": "CAN_DRIVE" }, "context": { "input_params": { "region": "north" } } } ``` Response - the call fails before evaluation. The error names the logical location and the parameter; the physical database alias is never exposed. **Response 422** ```json { "message": "Unprocessable Entity", "errors": [ "unknown location \"north\" for parameter \"$region\"" ] } ``` ### Step 5 Resource search scoped to the east location - which cars can alice drive there? Search endpoints take the location parameter exactly like evaluation. **POST https://eu.api.indykite.com/access/v1/search/resource** ```json { "subject": { "type": "Person", "id": "person-alice" }, "action": { "name": "CAN_DRIVE" }, "resource": { "type": "Car" }, "context": { "input_params": { "region": "east" } } } ``` Response - resource references from the east constituent. **Response 200** ```json { "results": [ { "type": "Car", "id": "car-kitt" } ] } ``` ### Step 6 Delete the policy when done. **DELETE https://eu.api.indykite.com/configs/v1/authorization-policies/{id}** ```json { "id": "your_policy_configuration_gid" } ``` ## Common Errors ### 422: unknown location "..." for parameter "$region" **Solution:** The location must be a key of the project's alias_mapping (global, east, west in this example). Send the logical location, never the Neo4j database name ### 422: location parameter "$region" requires a composite database, but the app space has none configured **Solution:** The policy declares graph.byName($param) but the project has no composite_db_name configured. Configure the composite connection (see residency-1) or use a policy without location routing ### 422: location parameter "$region" must be a non-empty string **Solution:** Every location parameter declared by the policy must arrive in context.input_params as a non-empty string ### 403: bearer token subject differs from requested subject **Solution:** On bearer-token calls to a 3.0-kbac policy, the token's subject must match the subject in the request. Send the decision request for the user the token belongs to --- Source: https://developer.indykite.com/resources/authz-8 --- # Ingest Data into the IndyKite Knowledge Graph (IKG) > Add nodes (entities) and relationships to the IndyKite Knowledge Graph using the Capture API. This is the foundation for all graph-based queries and authorization. **Category:** Capture **API:** Capture **Tags:** Data Ingestion, Nodes, Relationships, Knowledge Graph, Capture API **Last Updated:** 2026-03-20 **OpenAPI Endpoints:** /capture/v1/nodes, /capture/v1/relationships **Related Guides:** /guides/guide-sandbox, /guides/guide-environment ## Summary This guide demonstrates how to populate the IndyKite Knowledge Graph (IKG) with data: 1. Capture nodes - Create entities like Person, Company, Vehicle, Contract, LicenseNumber, PaymentMethod 2. Capture relationships - Connect nodes with typed relationships like OWNS, ACCEPTED, COVERS, HAS After ingestion, the data is available for: - ContX IQ queries (context-aware data retrieval) - KBAC authorization decisions (Knowledge-Based Access Control) - Hub Explorer visualization ## Use Case Scenario: You are building a vehicle rental application and need to store: - People who can rent vehicles - Companies that own vehicle fleets - Vehicles with license numbers - Contracts linking people to vehicles - Payment methods for billing The graph structure enables queries like "Which vehicles can Ryan access?" or "Who has a contract for vehicle ABC-123?" Example graph after ingestion: Person(Ryan) -[ACCEPTED]-> Contract1 -[COVERS]-> Vehicle(Car1) -[HAS]-> LicenseNumber(ABC-123) Company1 -[OWNS]-> Vehicle(Car1) Person(Ryan) -[HAS]-> PaymentMethod(Card1) ## Requirements Prerequisites: - Completed environment setup (see environment-1): Project, Application, Application Agent, and Credentials created - AppAgent credentials: The API key (X-IK-ClientKey) generated during environment setup Required API access: - POST /capture/v1/nodes/ (create/update nodes) - POST /capture/v1/relationships/ (create/update relationships) ## Steps Step 1: Ingest Nodes - Authentication: AppAgent credential as API key (header: X-IK-ClientKey) - Action: POST to /capture/v1/nodes/ with array of node definitions - Input: Each node requires: external_id (unique identifier), type (node label), and properties - Node types in this example: Person, Company, Vehicle, Contract, LicenseNumber, PaymentMethod - Result: Nodes created or updated in the IKG (upsert behavior based on external_id) Step 2: Ingest Relationships - Authentication: AppAgent credential as API key (header: X-IK-ClientKey) - Action: POST to /capture/v1/relationships/ with array of relationship definitions - Input: Each relationship requires: source node, target node, relationship type, and direction - Relationship types in this example: OWNS, ACCEPTED, COVERS, HAS, HAS_AGREEMENT_WITH - Result: Relationships created between existing nodes Step 3: Verify in Hub Explorer - Action: Open the IndyKite Hub and navigate to the Explorer - Result: Visual graph showing nodes and relationships ## Code Examples ### Step 1 POST request to capture nodes. Creates Person, Company, Vehicle, Contract, LicenseNumber, and PaymentMethod nodes. Each node has an external_id (unique identifier), type (label), and properties object. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "alice", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "alice@email.com" }, { "type": "given_name", "value": "Alice" }, { "type": "last_name", "value": "Smith" } ] }, { "external_id": "ryan", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "ryan@yahoo.co.uk" }, { "type": "given_name", "value": "ryan" }, { "type": "last_name", "value": "mushu" } ] }, { "external_id": "tilda", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "tilda@yahoo.co.uk" }, { "type": "given_name", "value": "tilda" }, { "type": "last_name", "value": "mushu" } ] }, { "external_id": "cb123", "type": "PaymentMethod", "properties": [ { "type": "payment_name", "value": "Credit Card" } ] }, { "external_id": "kl123", "type": "PaymentMethod", "properties": [ { "type": "payment_name", "value": "Klarna" } ] }, { "external_id": "ct123", "type": "Contract", "properties": [ { "type": "category", "value": "Insurance" }, { "type": "status", "value": "Active" }, { "type": "number", "value": "hfgrten123", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "ct234", "type": "Contract", "properties": [ { "type": "category", "value": "Insurance" }, { "type": "status", "value": "Active" }, { "type": "number", "value": "hfgrten234", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "ct985", "type": "Contract", "properties": [ { "type": "category", "value": "Insurance" }, { "type": "status", "value": "Active" }, { "type": "number", "value": "hfgrten985", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "car1", "type": "Vehicle", "properties": [ { "type": "category", "value": "Car" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "rtfhcnvjt471" } ] }, { "external_id": "car2", "type": "Vehicle", "properties": [ { "type": "category", "value": "Car" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "kdcbfrt178" } ] }, { "external_id": "truck1", "type": "Vehicle", "properties": [ { "type": "category", "value": "Truck" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "sncnrkcldp" } ] }, { "external_id": "license1", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "AX123456", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "license2", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "OL123456", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "license3", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "VN123456", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "company1", "type": "Company", "properties": [ { "type": "name", "value": "Company1" }, { "type": "registration", "value": "256314523" } ] }, { "external_id": "company2", "type": "Company", "properties": [ { "type": "name", "value": "Company2" }, { "type": "registration", "value": "942365123" } ] }, { "external_id": "application1", "type": "Application", "properties": [ { "type": "name", "value": "Application" } ] }, { "external_id": "application2", "type": "Application", "properties": [ { "type": "name", "value": "Application2" } ] } ] } ``` ### Step 2 POST request to capture relationships. Creates connections between nodes: Company-[OWNS]->Vehicle, Person-[ACCEPTED]->Contract, Contract-[COVERS]->Vehicle, Vehicle-[HAS]->LicenseNumber, Person-[HAS]->PaymentMethod. **POST https://eu.api.indykite.com/capture/v1/relationships/** ```json { "relationships": [ { "source": { "external_id": "ryan", "type": "Person" }, "target": { "external_id": "cb123", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "tilda", "type": "Person" }, "target": { "external_id": "kl123", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "cb123", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "ryan", "type": "Person" }, "target": { "external_id": "ct123", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "tilda", "type": "Person" }, "target": { "external_id": "ct234", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "ct985", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "ct123", "type": "Contract" }, "target": { "external_id": "car1", "type": "Vehicle" }, "type": "COVERS" }, { "source": { "external_id": "ct985", "type": "Contract" }, "target": { "external_id": "car1", "type": "Vehicle" }, "type": "COVERS" }, { "source": { "external_id": "ct234", "type": "Contract" }, "target": { "external_id": "truck1", "type": "Vehicle" }, "type": "COVERS" }, { "source": { "external_id": "car1", "type": "Vehicle" }, "target": { "external_id": "license1", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "truck1", "type": "Vehicle" }, "target": { "external_id": "license2", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "car2", "type": "Vehicle" }, "target": { "external_id": "license3", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "company1", "type": "Company" }, "target": { "external_id": "car1", "type": "Vehicle" }, "type": "OWNS" }, { "source": { "external_id": "company1", "type": "Company" }, "target": { "external_id": "car2", "type": "Vehicle" }, "type": "OWNS" }, { "source": { "external_id": "company1", "type": "Company" }, "target": { "external_id": "truck1", "type": "Vehicle" }, "type": "OWNS" }, { "source": { "external_id": "application1", "type": "Application" }, "target": { "external_id": "company1", "type": "Company" }, "type": "HAS_AGREEMENT_WITH" }, { "source": { "external_id": "application2", "type": "Application" }, "target": { "external_id": "company1", "type": "Company" }, "type": "HAS_AGREEMENT_WITH" } ] } ``` ## Common Errors ### 401: UNAUTHENTICATED **Solution:** Check X-IK-ClientKey header contains valid AppAgent credentials ### 400: INVALID_ARGUMENT **Solution:** Verify node/relationship structure: external_id, type, and properties are required --- Source: https://developer.indykite.com/resources/capture-1 --- # ContX IQ: Role-Based Access Control with User Tokens and Organizations > Advanced scenario using user access tokens as subjects for role-based authorization. Members can read Events, Admins can create Events. Demonstrates Token Introspect integration with ContX IQ policies. **Category:** ContX IQ **API:** ContX IQ **Tags:** ContX IQ Policy, ContX IQ Query, User Subject, Role-Based Access, Token Introspect, Organization, Events **Last Updated:** 2026-03-20 **OpenAPI Endpoints:** /capture/v1/nodes, /capture/v1/relationships, /configs/v1/authorization-policies, /configs/v1/knowledge-queries, /contx-iq/v1/execute **Related Guides:** /guides/guide-contx-iq, /guides/guide-sandbox ## Summary This advanced example combines Token Introspect with ContX IQ for user-based authorization: Key concepts: 1. User access tokens (from IdP) become subjects in ContX IQ queries 2. Token Introspect creates User nodes linked to UserProfile nodes 3. UserProfiles have roles (Member, Admin) in Organizations 4. Policies scope access based on role Workflow: 1. Introspect user tokens to create User nodes 2. Link Users to UserProfiles with roles 3. Member role: Can READ Events in their Organization 4. Admin role: Can CREATE new Events in their Organization This pattern enables multi-tenant, role-based access control. ## Use Case Scenario: Event management platform with role-based permissions. Organization structure: - Organization1 - UserProfile(Alice) with role: "Member" - UserProfile(Bob) with role: "Admin" - Event1, Event2 (existing events) User authentication: - Alice logs in -> Token introspected -> User(alice-token) created - Bob logs in -> Token introspected -> User(bob-token) created Graph after token introspection: User(alice-token) -[_SAME_AS]-> UserProfile(Alice) -[MEMBER_OF {role:"Member"}]-> Organization1 User(bob-token) -[_SAME_AS]-> UserProfile(Bob) -[MEMBER_OF {role:"Admin"}]-> Organization1 Organization1 -[HAS]-> Event1, Event2 Authorization results: - Alice (Member): Can READ Event1, Event2. Cannot CREATE events. - Bob (Admin): Can READ Event1, Event2. Can CREATE Event3. ## Requirements Prerequisites: - ServiceAccount credentials: For creating policies and queries (Bearer token) - AppAgent credentials: For data ingestion and query execution (X-IK-ClientKey) - User access tokens: JWT tokens for users (Alice, Bob) from your IdP - Token Introspect configuration: Set up to validate your IdP's tokens How user tokens are used: - Pass user token in Authorization header: "Bearer {user_access_token}" - Token Introspect validates and creates/updates User node - User node becomes the subject for policy evaluation ## Steps Step 1: Ingest Organization and Event Data - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: POST nodes: Organization, UserProfile (with roles), Event nodes - Action: POST relationships: MEMBER_OF (with role property), HAS - Result: Organization structure ready Step 2: Create User Linking Policy - Authentication: ServiceAccount credential (Bearer token) - Action: POST policy allowing: - READ on Subject nodes (for token introspection) - UPSERT on User nodes (created from tokens) - UPSERT on _SAME_AS relationships (User -> UserProfile) - Result: Policy ID returned Step 3: Create User Linking Query - Authentication: ServiceAccount credential (Bearer token) - Action: POST query that links introspected User to matching UserProfile - Result: Query ID returned Step 4: Execute with Alice's Token - Authentication: AppAgent credential + Alice's user token (Bearer header) - Action: POST to /contx-iq/v1/execute - Result: User(alice) created and linked to UserProfile(Alice) Step 5: Execute with Bob's Token - Authentication: AppAgent credential + Bob's user token (Bearer header) - Action: POST to /contx-iq/v1/execute - Result: User(bob) created and linked to UserProfile(Bob) Step 6: Create Role-Based Read Policy - Authentication: ServiceAccount credential (Bearer token) - Action: POST policy allowing Users to READ Events through: User -[_SAME_AS]-> UserProfile -[MEMBER_OF]-> Organization -[HAS]-> Event - Role filter: Applies to both "Member" and "Admin" roles - Result: Policy ID returned Step 7: Create Event Read Query - Authentication: ServiceAccount credential (Bearer token) - Action: POST query that reads Events for the authenticated user - Result: Query ID returned Step 8: Execute Read as Alice (Member) - Authentication: AppAgent credential + Alice's token - Action: POST to /contx-iq/v1/execute - Result: Event1, Event2 returned (Alice can read events) Step 9: Create Admin Write Policy - Authentication: ServiceAccount credential (Bearer token) - Action: POST policy allowing Users with Admin role to CREATE Events: User -[_SAME_AS]-> UserProfile -[MEMBER_OF {role:"Admin"}]-> Organization - Role filter: Only "Admin" role can upsert - Result: Policy ID returned Step 10: Create Event Write Query - Authentication: ServiceAccount credential (Bearer token) - Action: POST query that creates new Event and links to Organization - Parameters: $eventId, $eventName - Result: Query ID returned Step 11: Execute Write as Bob (Admin) - Authentication: AppAgent credential + Bob's token - Action: POST to /contx-iq/v1/execute with event parameters - Result: Event3 created and linked to Organization1 ## Code Examples ### Step 1 Capture the nodes needed for this use case. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "alice", "type": "UserProfile", "properties": [ { "type": "email", "value": "alice@email.com" } ] }, { "external_id": "bob", "type": "UserProfile", "properties": [ { "type": "email", "value": "bob@email.com" } ] }, { "external_id": "org1", "type": "Organization", "properties": [ { "type": "name", "value": "Org1" } ] }, { "external_id": "event_lambda", "type": "Event", "labels": [], "is_identity": false, "properties": [ { "type": "title", "value": "Event Lambda", "metadata": { "assurance_level": 1, "source": "Some Source", "verified_time": "2024-04-10T06:28:16Z" } }, { "type": "startDate", "value": "2025-01-01", "metadata": { "assurance_level": 1, "source": "Some Source", "verified_time": "2025-04-10T06:28:16Z" } }, { "type": "link", "value": "https://events.com", "metadata": { "assurance_level": 1, "source": "Some Source", "verified_time": "2025-04-10T06:28:16Z" } }, { "type": "description", "value": "Description ...", "metadata": { "assurance_level": 1, "source": "Some Source", "verified_time": "2025-04-10T06:28:16Z" } } ] } ] } ``` Capture the relationships needed for this use case. **POST https://eu.api.indykite.com/capture/v1/relationships/** ```json { "relationships": [ { "source": { "external_id": "alice", "type": "UserProfile" }, "target": { "external_id": "org1", "type": "Organization" }, "type": "BELONGS_TO", "properties": [ { "type": "role", "value": "Member" } ] }, { "source": { "external_id": "bob", "type": "UserProfile" }, "target": { "external_id": "org1", "type": "Organization" }, "type": "BELONGS_TO", "properties": [ { "type": "role", "value": "Admin" } ] }, { "source": { "external_id": "event_lambda", "type": "Event" }, "target": { "external_id": "org1", "type": "Organization" }, "type": "PART_OF" } ] } ``` ### Step 2 Create a CIQ Policy which designates the Subject nodes can be read, the User nodes can be upserted and relationships between User and UserProfile can be upserted. **policy.json** ```json { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "UserProfile" }, "condition": { "cypher": "MATCH (subject:UserProfile)", "filter": [ { "operator": "=", "attribute": "subject.external_id", "value": "$subject_external_id" } ] }, "allowed_reads": { "nodes": [ "subject", "subject.*" ] }, "allowed_upserts": { "nodes": { "node_types": [ "User" ] }, "relationships": { "relationship_types": [ { "type": "HAS", "source_node_label": "UserProfile", "target_node_label": "User" } ] } } } ``` Request to create the CIQ Policy configuration using REST. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"meta\":{\"policy_version\":\"1.0-ciq\"},\"subject\":{\"type\":\"UserProfile\"},\"condition\":{\"cypher\":\"MATCH (subject:UserProfile)\",\"filter\":[{\"operator\":\"=\",\"attribute\":\"subject.external_id\",\"value\":\"$subject_external_id\"}]},\"allowed_reads\":{\"nodes\":[\"subject\",\"subject.*\"]},\"allowed_upserts\":{\"nodes\":{\"node_types\":[\"User\"]},\"relationships\":{\"relationship_types\":[{\"type\":\"HAS\",\"source_node_label\":\"UserProfile\",\"target_node_label\":\"User\"}]}}}", "status": "ACTIVE", "tags": [] } ``` Request to read the CIQ Policy configuration using REST. **policy_request.json** ```json { "id": "your_policy_configuration_gid" } ``` ### Step 3 Create a CIQ Query in the context of the policy to retrieve and upsert the data. The labels: ["DigitalTwin"] entry creates the User as an identity node - the same result as is_identity: true in the Capture API. **knowledge_query.json** ```json { "nodes": [ "subject", "subject.property.email", "user.property.email" ], "relationships": [], "upsert_nodes": [ { "name": "user", "type": "User", "external_id": "$token.sub", "labels": [ "DigitalTwin" ], "properties": [ { "type": "email", "value": "$email" } ] } ], "upsert_relationships": [ { "name": "has", "source": "subject", "target": "user", "type": "HAS" } ] } ``` Request to create a CIQ Query configuration using REST. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "description of knowledge query", "display_name": "knowledge query name", "name": "knowledge-query-name", "policy_id": "your_policy_gid", "query": "{\"nodes\":[\"subject\",\"subject.property.email\",\"user.property.email\"],\"relationships\":[],\"upsert_nodes\":[{\"name\":\"user\",\"type\":\"User\",\"external_id\":\"$token.sub\",\"labels\":[\"DigitalTwin\"],\"properties\":[{\"type\":\"email\",\"value\":\"$email\"}]}],\"upsert_relationships\":[{\"name\":\"has\",\"source\":\"subject\",\"target\":\"user\",\"type\":\"HAS\"}]}", "status": "ACTIVE" } ``` Read the CIQ Query Configuration. **GET https://eu.api.indykite.com/configs/v1/knowledge-queries/{id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 4 Run a CIQ Execution to create a User node from the first access token and link it to the corresponding UserProfile. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "knowledge_query_gid", "input_params": { "subject_external_id": "alice", "user_external_id": "alice_user_external_id", "email": "alice@email.com" }, "page_token": 1 } ``` CIQ Execution response. **response.json** ```json { "data": [ { "nodes": { "subject": { "Id": 14, "ElementId": "4:0f1c76a0-92f1-4474-80af-aa4c317e636a:14", "Labels": [ "Unique", "Resource", "UserProfile" ], "Props": { "_service": "capture-api", "create_time": "2025-06-13T16:08:25.47Z", "external_id": "alice", "id": "4H1gEySmTGasbkjWDyyuXg", "type": "UserProfile", "update_time": "2025-06-13T16:08:25.47Z" } }, "subject.property.email": "alice@email.com", "user.property.email": "alice@email.com" } } ] } ``` ### Step 5 Run a CIQ Execution to create a User node from the second access token and link it to the corresponding UserProfile. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "knowledge_query_gid", "input_params": { "subject_external_id": "bob", "user_external_id": "bob_user_external_id", "email": "bob@email.com" }, "page_token": 1 } ``` CIQ Execution response. **response.json** ```json { "data": [ { "nodes": { "subject": { "Id": 15, "ElementId": "4:0f1c76a0-92f1-4474-80af-aa4c317e636a:15", "Labels": [ "Unique", "Resource", "UserProfile" ], "Props": { "_service": "capture-api", "create_time": "2025-06-13T16:08:25.47Z", "external_id": "bob", "id": "lIHALUiJSSiwVCff51KxxA", "type": "UserProfile", "update_time": "2025-06-13T16:08:25.47Z" } }, "subject.property.email": "bob@email.com", "user.property.email": "bob@email.com" } } ] } ``` ### Step 6 Create a CIQ Policy which designates the nodes which are allowed to read the Event nodes linked to the Organization nodes they have a relationship with, according to a specific role. **policy.json** ```json { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "UserProfile" }, "condition": { "cypher": "MATCH (user:User)<-[:HAS]-(subject:UserProfile)-[bt:BELONGS_TO]->(org:Organization)<-[:PART_OF]-(event:Event)", "filter": [ { "operator": "AND", "operands": [ { "operator": "=", "attribute": "subject.external_id", "value": "$subject_external_id" }, { "operator": "=", "attribute": "user.external_id", "value": "$token.sub" }, { "operator": "=", "attribute": "bt.role", "value": "Member" } ] } ] }, "allowed_reads": { "nodes": [ "event", "event.*", "org.external_id", "subject.property.name" ] } } ``` Json to create the CIQ Policy configuration using REST. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"meta\":{\"policy_version\":\"1.0-ciq\"},\"subject\":{\"type\":\"UserProfile\"},\"condition\":{\"cypher\":\"MATCH (user:User)<-[:HAS]-(subject:UserProfile)-[bt:BELONGS_TO]->(org:Organization)<-[:PART_OF]-(event:Event)\",\"filter\":[{\"operator\":\"AND\",\"operands\":[{\"operator\":\"=\",\"attribute\":\"subject.external_id\",\"value\":\"$subject_external_id\"},{\"operator\":\"=\",\"attribute\":\"user.external_id\",\"value\":\"$token.sub\"},{\"operator\":\"=\",\"attribute\":\"bt.role\",\"value\":\"Member\"}]}]},\"allowed_reads\":{\"nodes\":[\"event\",\"event.*\",\"org.external_id\",\"subject.property.name\"]}}", "status": "ACTIVE", "tags": [] } ``` Json to read the CIQ Policy configuration using REST. **policy_request.json** ```json { "id": "your_policy_configuration_gid" } ``` ### Step 7 Create a CIQ Query in the context of the policy to retrieve data. **knowledge_query.json** ```json { "nodes": [ "event" ], "filter": { "attribute": "subject.property.email", "operator": "=", "value": "$email" } } ``` Json to create a CIQ Query configuration using REST. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "description of knowledge query", "display_name": "knowledge query name", "name": "knowledge-query-name", "policy_id": "your_policy_gid", "query": "{\"nodes\":[\"event\"],\"filter\":{\"attribute\":\"subject.property.email\",\"operator\":\"=\",\"value\":\"$email\"}}", "status": "ACTIVE" } ``` Read the CIQ Query Configuration. **GET https://eu.api.indykite.com/configs/v1/knowledge-queries/{id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 8 Run a CIQ Execution to read the data. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "knowledge_query_gid", "input_params": { "email": "alice@email.com", "subject_external_id": "alice" }, "page_token": 1 } ``` CIQ Execution response. **response.json** ```json { "data": [ { "nodes": { "event": { "Id": 8, "ElementId": "4:0f1c76a0-92f1-4474-80af-aa4c317e636a:8", "Labels": [ "Unique", "Resource", "Event" ], "Props": { "_service": "capture-api", "create_time": "2025-06-13T16:08:11.293Z", "external_id": "event_lambda", "id": "ABeHsubWR7a3Yj0lOSuKRA", "type": "Event", "update_time": "2025-06-13T16:08:11.293Z" } } } } ] } ``` ### Step 9 Create a CIQ Policy which designates the nodes which are allowed to upsert Event nodes linked to the Organization nodes they have a relationship with, according to a specific role. **policy.json** ```json { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "UserProfile" }, "condition": { "cypher": "MATCH (user:User)<-[:HAS]-(subject:UserProfile)-[bt:BELONGS_TO]->(org:Organization)", "filter": [ { "operator": "AND", "operands": [ { "operator": "=", "attribute": "subject.external_id", "value": "$profile_external_id" }, { "operator": "=", "attribute": "$token.sub", "value": "$user_external_id" }, { "operator": "=", "attribute": "bt.role", "value": "Admin" } ] } ] }, "allowed_reads": { "nodes": [ "org.external_id", "subject.property.name" ] }, "allowed_upserts": { "nodes": { "node_types": [ "Event" ] }, "relationships": { "relationship_types": [ { "type": "PART_OF", "source_node_label": "Event", "target_node_label": "Organization" } ] } } } ``` Json to create the CIQ Policy configuration using REST. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"meta\":{\"policy_version\":\"1.0-ciq\"},\"subject\":{\"type\":\"UserProfile\"},\"condition\":{\"cypher\":\"MATCH (user:User)<-[:HAS]-(subject:UserProfile)-[bt:BELONGS_TO]->(org:Organization)\",\"filter\":[{\"operator\":\"AND\",\"operands\":[{\"operator\":\"=\",\"attribute\":\"subject.external_id\",\"value\":\"$profile_external_id\"},{\"operator\":\"=\",\"attribute\":\"$token.sub\",\"value\":\"$user_external_id\"},{\"operator\":\"=\",\"attribute\":\"bt.role\",\"value\":\"Admin\"}]}]},\"allowed_reads\":{\"nodes\":[\"org.external_id\",\"subject.property.name\"]},\"allowed_upserts\":{\"nodes\":{\"node_types\":[\"Event\"]},\"relationships\":{\"relationship_types\":[{\"type\":\"PART_OF\",\"source_node_label\":\"Event\",\"target_node_label\":\"Organization\"}]}}}", "status": "ACTIVE", "tags": [] } ``` Json to read the CIQ Policy configuration using REST. **policy_request.json** ```json { "id": "your_policy_configuration_gid" } ``` ### Step 10 Create a CIQ Query in the context of the policy to upsert an Event node. **knowledge_query.json** ```json { "nodes": [ "event", "event.property.title", "event.property.link" ], "relationships": [], "upsert_nodes": [ { "name": "event", "type": "Event", "external_id": "$eventId", "properties": [ { "type": "title", "value": "$title" }, { "type": "link", "value": "$link" } ] } ], "upsert_relationships": [ { "name": "part", "source": "event", "target": "org", "type": "PART_OF" } ] } ``` Json to create a CIQ Query configuration using REST. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "description of knowledge query", "display_name": "knowledge query name", "name": "knowledge-query-name", "policy_id": "your_policy_gid", "query": "{\"nodes\":[\"event\",\"event.property.title\",\"event.property.link\"],\"relationships\":[],\"upsert_nodes\":[{\"name\":\"event\",\"type\":\"Event\",\"external_id\":\"$eventId\",\"properties\":[{\"type\":\"title\",\"value\":\"$title\"},{\"type\":\"link\",\"value\":\"$link\"}]}],\"upsert_relationships\":[{\"name\":\"part\",\"source\":\"event\",\"target\":\"org\",\"type\":\"PART_OF\"}]}", "status": "ACTIVE" } ``` Read the CIQ Query Configuration. **GET https://eu.api.indykite.com/configs/v1/knowledge-queries/{id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 11 Run a CIQ Execution to upsert an Event node. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "knowledge_query_gid", "input_params": { "profile_external_id": "bob", "user_external_id": "bob_user_external_id", "eventId": "event14", "title": "Event in the parking lot", "link": "https://www.example.com" }, "page_token": 1 } ``` CIQ Execution response. **response.json** ```json { "data": [ { "nodes": { "event": { "Id": 24, "ElementId": "4:0f1c76a0-92f1-4474-80af-aa4c317e636a:24", "Labels": [ "Unique", "Resource", "Event" ], "Props": { "create_time": "2025-06-13T16:23:53.388Z", "external_id": "event14", "id": "sYE-gBnaRQeRO6gZdHA59A", "type": "Event", "update_time": "2025-06-13T16:23:53.388Z" } }, "event.property.link": "https://www.example.com", "event.property.title": "Event in the parking lot" } } ] } ``` ## Common Errors ### 401: UNAUTHENTICATED **Solution:** Check credentials: ServiceAccount Bearer token for config APIs, X-IK-ClientKey for execution ### 404: NOT_FOUND **Solution:** Verify the query/policy ID exists and belongs to your project --- Source: https://developer.indykite.com/resources/ciq-10 --- # ContX IQ: User Consent Management - Grant and Revoke Payment Access > Demonstrates user-controlled consent workflows where a Person can authorize a Company to access their payment method, and later revoke that access. Uses user tokens as subjects for self-service data governance. **Category:** ContX IQ **API:** ContX IQ **Tags:** ContX IQ Policy, ContX IQ Query, Consent Management, User Subject, Grant Access, Revoke Access, Payment Method, GDPR **Last Updated:** 2026-03-20 **OpenAPI Endpoints:** /capture/v1/nodes, /capture/v1/relationships, /configs/v1/authorization-policies, /configs/v1/knowledge-queries, /contx-iq/v1/execute **Related Guides:** /guides/guide-contx-iq, /guides/guide-sandbox ## Summary This example demonstrates user-controlled consent management: Use case: A Person grants a Company permission to charge their payment method, then later revokes it. Key concepts: 1. User authenticates with their token -> becomes the query subject 2. User can only manage consent for their OWN payment methods 3. GRANTED relationship represents active consent 4. Deleting GRANTED relationship revokes consent Operations: - Grant: Create PaymentMethod -[GRANTED]-> Company relationship - Revoke: Delete PaymentMethod -[GRANTED]-> Company relationship This pattern supports GDPR consent requirements and user self-service. ## Use Case Scenario: Alice wants to let RentalCo charge her credit card for rentals. Initial graph: Person(Alice) -[HAS]-> PaymentMethod(AliceCard) Company(RentalCo) Grant consent workflow: 1. Alice authenticates (token introspected) 2. Alice executes grant query 3. Result: PaymentMethod(AliceCard) -[GRANTED]-> Company(RentalCo) Later, Alice revokes consent: 1. Alice authenticates again 2. Alice executes revoke query 3. Result: GRANTED relationship deleted Authorization ensures: - Alice can only grant/revoke consent for HER payment methods - The system verifies Person -[HAS]-> PaymentMethod relationship - Other users cannot modify Alice's consents ## Requirements Prerequisites: - ServiceAccount credentials: For creating policies and queries (Bearer token) - AppAgent credentials: For data ingestion and query execution (X-IK-ClientKey) - User access token: JWT for the Person granting/revoking consent How to pass user token: - Header: Authorization: Bearer {user_access_token} - The token identifies the Person and scopes operations to their data ## Steps Step 1: Ingest Person, PaymentMethod, and Company Data - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: POST Person, PaymentMethod, Company nodes - Action: POST Person -[HAS]-> PaymentMethod relationship - Result: Graph ready for consent management Step 2: Create Consent Grant Policy - Authentication: ServiceAccount credential (Bearer token) - Action: POST policy allowing authenticated Person to: - UPSERT GRANTED relationship from their PaymentMethod to a Company - Authorization path: Subject -[HAS]-> PaymentMethod -[GRANTED]-> Company - Result: Policy ID returned Step 3: Create Consent Grant Query - Authentication: ServiceAccount credential (Bearer token) - Action: POST query that creates PaymentMethod -[GRANTED]-> Company - Parameters: $companyId (which company to grant access to) - Result: Query ID returned Step 4: Execute Consent Grant (as Alice) - Authentication: AppAgent credential + Alice's token (Bearer header) - Action: POST to /contx-iq/v1/execute with companyId parameter - Result: AliceCard -[GRANTED]-> RentalCo relationship created Step 5: Create Consent Revoke Policy - Authentication: ServiceAccount credential (Bearer token) - Action: POST policy allowing authenticated Person to: - DELETE GRANTED relationship from their PaymentMethod to a Company - Result: Policy ID returned Step 6: Create Consent Revoke Query - Authentication: ServiceAccount credential (Bearer token) - Action: POST query that deletes PaymentMethod -[GRANTED]-> Company - Parameters: $companyId (which company to revoke access from) - Result: Query ID returned Step 7: Execute Consent Revoke (as Alice) - Authentication: AppAgent credential + Alice's token (Bearer header) - Action: POST to /contx-iq/v1/execute with companyId parameter - Result: AliceCard -[GRANTED]-> RentalCo relationship deleted ## Code Examples ### Step 1 Capture the nodes needed for this use case. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "alice", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "alice@email.com" }, { "type": "given_name", "value": "Alice" }, { "type": "last_name", "value": "Smith" } ] }, { "external_id": "bob", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "bob@email.com" }, { "type": "given_name", "value": "Bob" } ] }, { "external_id": "cb123", "type": "PaymentMethod", "properties": [ { "type": "payment_name", "value": "Credit Card" } ] }, { "external_id": "kl123", "type": "PaymentMethod", "properties": [ { "type": "payment_name", "value": "Klarna" } ] }, { "external_id": "ct123", "type": "Contract", "properties": [ { "type": "category", "value": "Parking" }, { "type": "status", "value": "Active" } ] }, { "external_id": "ct234", "type": "Contract", "properties": [ { "type": "category", "value": "Parking" }, { "type": "status", "value": "Active" } ] }, { "external_id": "car1", "type": "Vehicle", "properties": [ { "type": "category", "value": "Car" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "rtfhcnvjt471" } ] }, { "external_id": "car2", "type": "Vehicle", "properties": [ { "type": "category", "value": "Car" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "kdcbfrt178" } ] }, { "external_id": "license1", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "AX123456", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "license2", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "OL123456", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "companyParking", "type": "Company", "properties": [ { "type": "name", "value": "City Parking Inc" } ] } ] } ``` Capture the relationships needed for this use case. **POST https://eu.api.indykite.com/capture/v1/relationships/** ```json { "relationships": [ { "source": { "external_id": "bob", "type": "Person" }, "target": { "external_id": "kl123", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "cb123", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "ct123", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "bob", "type": "Person" }, "target": { "external_id": "ct234", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "ct123", "type": "Contract" }, "target": { "external_id": "car1", "type": "Vehicle" }, "type": "COVERS" }, { "source": { "external_id": "ct234", "type": "Contract" }, "target": { "external_id": "car2", "type": "Vehicle" }, "type": "COVERS" }, { "source": { "external_id": "car1", "type": "Vehicle" }, "target": { "external_id": "license1", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "car2", "type": "Vehicle" }, "target": { "external_id": "license2", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "companyParking", "type": "Company" }, "target": { "external_id": "ct234", "type": "Contract" }, "type": "OFFERS" }, { "source": { "external_id": "companyParking", "type": "Company" }, "target": { "external_id": "ct123", "type": "Contract" }, "type": "OFFERS" } ] } ``` ### Step 2 Create a CIQ Policy which designates a relationship between a PaymentMethod and a Company can be upserted. **policy.json** ```json { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "Person" }, "condition": { "cypher": "MATCH (company:Company)-[:OFFERS]->(contract:Contract)<-[:ACCEPTED]-(subject:Person)-[:HAS]->(payment:PaymentMethod), (contract)-[:COVERS]->(vehicle:Vehicle)-[:HAS]->(ln:LicenseNumber)", "filter": [ { "operator": "AND", "operands": [ { "attribute": "subject.external_id", "operator": "=", "value": "$subject_external_id" }, { "attribute": "$token.sub", "operator": "=", "value": "$token_sub" } ] } ] }, "allowed_reads": { "nodes": [ "company.*", "subject.*", "payment.*" ] }, "allowed_upserts": { "relationships": { "relationship_types": [ { "type": "GRANTED", "source_node_label": "Company", "target_node_label": "PaymentMethod" } ] } } } ``` Request to create the CIQ Policy configuration using REST. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"meta\":{\"policy_version\":\"1.0-ciq\"},\"subject\":{\"type\":\"Person\"},\"condition\":{\"cypher\":\"MATCH (company:Company)-[:OFFERS]->(contract:Contract)<-[:ACCEPTED]-(subject:Person)-[:HAS]->(payment:PaymentMethod), (contract)-[:COVERS]->(vehicle:Vehicle)-[:HAS]->(ln:LicenseNumber)\",\"filter\":[{\"operator\":\"AND\",\"operands\":[{\"attribute\":\"subject.external_id\",\"operator\":\"=\",\"value\":\"$subject_external_id\"},{\"attribute\":\"$token.sub\",\"operator\":\"=\",\"value\":\"$token_sub\"}]}]},\"allowed_reads\":{\"nodes\":[\"company.*\",\"subject.*\",\"payment.*\"]},\"allowed_upserts\":{\"relationships\":{\"relationship_types\":[{\"type\":\"GRANTED\",\"source_node_label\":\"Company\",\"target_node_label\":\"PaymentMethod\"}]}}}", "status": "ACTIVE", "tags": [] } ``` Request to read the CIQ Policy configuration using REST. **policy_request.json** ```json { "id": "your_policy_configuration_gid" } ``` ### Step 3 Create a CIQ Query in the context of the policy to create a GRANTED relationship between a company and a payment method. **knowledge_query.json** ```json { "nodes": [ "company.external_id", "subject.external_id", "payment.external_id" ], "upsert_relationships": [ { "name": "newRel", "source": "company", "target": "payment", "type": "GRANTED" } ] } ``` Request to create a CIQ Query configuration using REST. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "description of knowledge query", "display_name": "knowledge query name", "name": "knowledge-query-name", "policy_id": "your_policy_gid", "query": "{\"nodes\":[\"company.external_id\",\"subject.external_id\",\"payment.external_id\"],\"upsert_relationships\":[{\"name\":\"newRel\",\"source\":\"company\",\"target\":\"payment\",\"type\":\"GRANTED\"}]}", "status": "ACTIVE" } ``` Read the CIQ Query Configuration. **GET https://eu.api.indykite.com/configs/v1/knowledge-queries/{id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 4 Run a CIQ Execution to create a GRANTED relationship from the Person access token. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "knowledge_query_gid", "input_params": { "subject_external_id": "alice", "token_sub": "alice_user_external_id" }, "page_token": 1 } ``` CIQ Execution response. **response.json** ```json { "data": [ { "nodes": { "company.external_id": "companyParking", "payment.external_id": "cb123", "subject.external_id": "alice" } } ] } ``` ### Step 5 Create a CIQ Policy which designates a relationship between a PaymentMethod and a Company can be deleted. **policy.json** ```json { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "Person" }, "condition": { "cypher": "MATCH (company:Company)-[g1:GRANTED]->(payment:PaymentMethod)<-[:HAS]-(subject:Person)", "filter": [ { "operator": "AND", "operands": [ { "attribute": "subject.external_id", "operator": "=", "value": "$subject_external_id" }, { "attribute": "$token.sub", "operator": "=", "value": "$token_sub" } ] } ] }, "allowed_reads": { "nodes": [ "company.*", "subject.*", "payment.*" ] }, "allowed_deletes": { "relationships": [ "g1" ] } } ``` Json to create the CIQ Policy configuration using REST. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"meta\":{\"policy_version\":\"1.0-ciq\"},\"subject\":{\"type\":\"Person\"},\"condition\":{\"cypher\":\"MATCH (company:Company)-[g1:GRANTED]->(payment:PaymentMethod)<-[:HAS]-(subject:Person)\",\"filter\":[{\"operator\":\"AND\",\"operands\":[{\"attribute\":\"subject.external_id\",\"operator\":\"=\",\"value\":\"$subject_external_id\"},{\"attribute\":\"$token.sub\",\"operator\":\"=\",\"value\":\"$token_sub\"}]}]},\"allowed_reads\":{\"nodes\":[\"company.*\",\"subject.*\",\"payment.*\"]},\"allowed_deletes\":{\"relationships\":[\"g1\"]}}", "status": "ACTIVE", "tags": [] } ``` Json to read the CIQ Policy configuration using REST. **policy_request.json** ```json { "id": "your_policy_configuration_gid" } ``` ### Step 6 Create a CIQ Query in the context of the policy to delete a GRANTED relationship between a company and a payment method. **knowledge_query.json** ```json { "nodes": [ "company.external_id", "subject.external_id", "payment.external_id" ], "delete_relationships": [ "g1" ] } ``` Json to create a CIQ Query configuration using REST. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "description of knowledge query", "display_name": "knowledge query name", "name": "knowledge-query-name", "policy_id": "your_policy_gid", "query": "{\"nodes\":[\"company.external_id\",\"subject.external_id\",\"payment.external_id\"],\"delete_relationships\":[\"g1\"]}", "status": "ACTIVE" } ``` Read the CIQ Query Configuration. **GET https://eu.api.indykite.com/configs/v1/knowledge-queries/{id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 7 Run a CIQ Execution to delete a GRANTED relationship from the Person access token. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "knowledge_query_gid", "input_params": { "subject_external_id": "alice", "token_sub": "alice_user_external_id" }, "page_token": 1 } ``` CIQ Execution response. **response.json** ```json { "data": [ { "nodes": { "company.external_id": "companyParking", "payment.external_id": "cb123", "subject.external_id": "alice" } } ] } ``` ## Common Errors ### 401: UNAUTHENTICATED **Solution:** Check credentials: ServiceAccount Bearer token for config APIs, X-IK-ClientKey for execution ### 404: NOT_FOUND **Solution:** Verify the query/policy ID exists and belongs to your project --- Source: https://developer.indykite.com/resources/ciq-11 --- # ContX IQ: Step-Up Authentication Based on auth_time Claim > Enforce re-authentication for sensitive operations by checking the token's auth_time claim. If the user authenticated more than one hour ago, return an advice requesting fresh authentication. **Category:** ContX IQ **API:** ContX IQ **Tags:** ContX IQ Policy, ContX IQ Query, Step-Up Authentication, auth_time, Session Freshness, Security, Advice **Last Updated:** 2026-03-20 **OpenAPI Endpoints:** /capture/v1/nodes, /capture/v1/relationships, /configs/v1/authorization-policies, /configs/v1/knowledge-queries, /contx-iq/v1/execute **Related Guides:** /guides/guide-contx-iq, /guides/guide-sandbox ## Summary This example enforces session freshness using the auth_time JWT claim: What is auth_time? The auth_time claim indicates when the user last actively authenticated (entered credentials). It's different from iat (issued at) - a refresh token can have a recent iat but old auth_time. Policy logic: - Calculate time since authentication: now() - auth_time - If > 1 hour: Return advice "Please re-authenticate" - If <= 1 hour: Allow the operation Use case: Sensitive operations (payment changes, account deletion) require recent authentication. ## Use Case Scenario: A user wants to change their payment method, which requires recent authentication. Token claims: - auth_time: 2024-01-15T10:00:00Z (when user entered password) - Current time: 2024-01-15T11:30:00Z - Time since auth: 1.5 hours (exceeds 1 hour limit) Request flow: 1. User (with valid but stale token) requests payment change 2. Policy checks: auth_time > 1 hour ago? 3. Response: {decision: false, advice: {action: "reauthenticate", reason: "session_too_old"}} 4. Application prompts user to sign in again 5. User re-authenticates, gets new token with fresh auth_time 6. Same request now succeeds This pattern ensures sensitive operations have fresh authentication without invalidating the entire session. ## Requirements Prerequisites: - ServiceAccount credentials: For creating policies and queries (Bearer token) - AppAgent credentials: For data ingestion and query execution (X-IK-ClientKey) - User access token: JWT with auth_time claim from your IdP - Token Introspect configuration: Must extract auth_time claim JWT auth_time claim: - Standard OIDC claim indicating last authentication time - Set by IdP when user actively authenticates - Not updated on token refresh (unlike iat) ## Steps Step 1: Ingest Person and PaymentMethod Data - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: POST Person, PaymentMethod, Company nodes and relationships - Result: Graph ready for payment operations Step 2: Create Step-Up Policy with auth_time Check - Authentication: ServiceAccount credential (Bearer token) - Action: POST policy with conditions: - Check: auth_age parameter (calculated from auth_time at introspection) - If auth_age > 3600 (seconds): Return advice to re-authenticate - If auth_age <= 3600: Allow operation - Result: Policy ID returned Step 3: Create Payment Change Query - Authentication: ServiceAccount credential (Bearer token) - Action: POST query for payment method operations - The query will either succeed or return step-up advice based on auth_time - Result: Query ID returned Step 4: Execute with Stale Token - Authentication: AppAgent credential + User token (with old auth_time) - Action: POST to /contx-iq/v1/execute - Result: {decision: false, advice: {reauthenticate: true}} if auth_time too old - Result: Success if auth_time is recent ## Code Examples ### Step 1 Capture the nodes needed for this use case. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "alice", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "alice@email.com" }, { "type": "given_name", "value": "Alice" }, { "type": "last_name", "value": "Smith" } ] }, { "external_id": "bob", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "bob@email.com" }, { "type": "given_name", "value": "Bob" } ] }, { "external_id": "cb123", "type": "PaymentMethod", "properties": [ { "type": "payment_name", "value": "Credit Card" } ] }, { "external_id": "kl123", "type": "PaymentMethod", "properties": [ { "type": "payment_name", "value": "Klarna" } ] }, { "external_id": "ct123", "type": "Contract", "properties": [ { "type": "category", "value": "Parking" }, { "type": "status", "value": "Active" } ] }, { "external_id": "ct234", "type": "Contract", "properties": [ { "type": "category", "value": "Parking" }, { "type": "status", "value": "Active" } ] }, { "external_id": "car1", "type": "Vehicle", "properties": [ { "type": "category", "value": "Car" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "rtfhcnvjt471" } ] }, { "external_id": "car2", "type": "Vehicle", "properties": [ { "type": "category", "value": "Car" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "kdcbfrt178" } ] }, { "external_id": "license1", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "AX123456", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "license2", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "OL123456", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "companyParking", "type": "Company", "properties": [ { "type": "name", "value": "City Parking Inc" } ] } ] } ``` Capture the relationships needed for this use case. **POST https://eu.api.indykite.com/capture/v1/relationships/** ```json { "relationships": [ { "source": { "external_id": "bob", "type": "Person" }, "target": { "external_id": "kl123", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "cb123", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "ct123", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "bob", "type": "Person" }, "target": { "external_id": "ct234", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "ct123", "type": "Contract" }, "target": { "external_id": "car1", "type": "Vehicle" }, "type": "COVERS" }, { "source": { "external_id": "ct234", "type": "Contract" }, "target": { "external_id": "car2", "type": "Vehicle" }, "type": "COVERS" }, { "source": { "external_id": "car1", "type": "Vehicle" }, "target": { "external_id": "license1", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "car2", "type": "Vehicle" }, "target": { "external_id": "license2", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "companyParking", "type": "Company" }, "target": { "external_id": "ct234", "type": "Contract" }, "type": "OFFERS" }, { "source": { "external_id": "companyParking", "type": "Company" }, "target": { "external_id": "ct123", "type": "Contract" }, "type": "OFFERS" } ] } ``` ### Step 2 Create a CIQ Policy which designates an advice step up if the access token auth_time claim is more than an hour ago using the parameter auth_age which is set at token introspection time. **policy.json** ```json { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "Person" }, "condition": { "cypher": "MATCH (company:Company)-[:OFFERS]->(contract:Contract)<-[:ACCEPTED]-(subject:Person)-[:HAS]->(payment:PaymentMethod), (contract)-[:COVERS]->(vehicle:Vehicle)-[:HAS]->(ln:LicenseNumber)", "filter": [ { "operator": "AND", "operands": [ { "attribute": "subject.external_id", "operator": "=", "value": "$subject_external_id" }, { "attribute": "$token.sub", "operator": "=", "value": "$token_sub" } ] } ], "token_filter": { "operator": "<=", "attribute": "$token.auth_age", "value": "3600", "advice": { "error": "insufficient_user_authentication", "error_description": "More recent authentication is required, max_age= 3600" } } }, "allowed_reads": { "nodes": [ "company.*", "subject.*", "payment.*" ] } } ``` Request to create the CIQ Policy configuration using REST. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"meta\":{\"policy_version\":\"1.0-ciq\"},\"subject\":{\"type\":\"Person\"},\"condition\":{\"cypher\":\"MATCH (company:Company)-[:OFFERS]->(contract:Contract)<-[:ACCEPTED]-(subject:Person)-[:HAS]->(payment:PaymentMethod), (contract)-[:COVERS]->(vehicle:Vehicle)-[:HAS]->(ln:LicenseNumber)\",\"filter\":[{\"operator\":\"AND\",\"operands\":[{\"attribute\":\"subject.external_id\",\"operator\":\"=\",\"value\":\"$subject_external_id\"},{\"attribute\":\"$token.sub\",\"operator\":\"=\",\"value\":\"$token_sub\"}]}],\"token_filter\":{\"operator\":\"<=\",\"attribute\":\"$token.auth_age\",\"value\":\"3600\",\"advice\":{\"error\":\"insufficient_user_authentication\",\"error_description\":\"More recent authentication is required, max_age= 3600\"}}},\"allowed_reads\":{\"nodes\":[\"company.*\",\"subject.*\",\"payment.*\"]}}", "status": "ACTIVE", "tags": [] } ``` Request to read the CIQ Policy configuration using REST. **policy_request.json** ```json { "id": "your_policy_configuration_gid" } ``` ### Step 3 Create a CIQ Query in the context of the policy to configure the step up advice. **knowledge_query.json** ```json { "nodes": [ "company.external_id", "payment.external_id" ] } ``` Request to create a CIQ Query configuration using REST. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "description of knowledge query", "display_name": "knowledge query name", "name": "knowledge-query-name", "policy_id": "your_policy_gid", "query": "{\"nodes\":[\"company.external_id\",\"payment.external_id\"]}", "status": "ACTIVE" } ``` Read the CIQ Query Configuration. **GET https://eu.api.indykite.com/configs/v1/knowledge-queries/{id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 4 Run a CIQ Execution to trigger the step up advice. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "knowledge_query_gid", "input_params": { "subject_external_id": "alice", "token_sub": "alice_user_external_id" }, "page_token": 1 } ``` CIQ Execution response if the auth_time is too old. **response.json** ```json { "message": "Unauthorized: invalid token. see response Www-Authenticate headers" } ``` ## Common Errors ### 401: UNAUTHENTICATED **Solution:** Check credentials: ServiceAccount Bearer token for config APIs, X-IK-ClientKey for execution ### 404: NOT_FOUND **Solution:** Verify the query/policy ID exists and belongs to your project --- Source: https://developer.indykite.com/resources/ciq-12 --- # ContX IQ: Step-Up Authentication Based on Token Issue Time (iat Claim) > Enforce token freshness by checking the iat (issued at) claim. If the token was issued more than one hour ago, return an advice requesting a new token. Different from auth_time - this checks token age, not authentication age. **Category:** ContX IQ **API:** ContX IQ **Tags:** ContX IQ Policy, ContX IQ Query, Step-Up Authentication, iat Claim, Token Freshness, Security, Advice **Last Updated:** 2026-03-20 **OpenAPI Endpoints:** /capture/v1/nodes, /capture/v1/relationships, /configs/v1/authorization-policies, /configs/v1/knowledge-queries, /contx-iq/v1/execute **Related Guides:** /guides/guide-contx-iq, /guides/guide-sandbox ## Summary This example enforces token freshness using the iat (issued at) JWT claim: What is iat? The iat claim indicates when the token was issued/created. Updated on every token refresh (unlike auth_time which tracks actual authentication). Difference from auth_time (ciq-12): - iat: When was this specific token created? - auth_time: When did the user last enter their credentials? Policy logic: - Calculate token age: now() - iat - If > 1 hour: Return advice "Get a fresh token" - If <= 1 hour: Allow the operation Use case: Ensure tokens are recent, preventing use of old/potentially compromised tokens. ## Use Case Scenario: API requires tokens issued within the last hour for all operations. Token claims: - iat: 2024-01-15T09:00:00Z (when token was created) - Current time: 2024-01-15T10:30:00Z - Token age: 1.5 hours (exceeds 1 hour limit) Request flow: 1. User makes request with valid but old token 2. Policy checks: iat > 1 hour ago? 3. Response: {decision: false, advice: {action: "refresh_token", reason: "token_too_old"}} 4. Application uses refresh token to get new access token (new iat) 5. Same request now succeeds with fresh token Compare with ciq-12: - ciq-12 (auth_time): User must re-enter password - ciq-13 (iat): Application can silently refresh token Use iat for: General token freshness Use auth_time for: Sensitive operations requiring active user presence ## Requirements Prerequisites: - ServiceAccount credentials: For creating policies and queries (Bearer token) - AppAgent credentials: For data ingestion and query execution (X-IK-ClientKey) - User access token: JWT with iat claim (standard JWT claim) - Token Introspect configuration: Extracts iat claim automatically JWT iat claim: - Standard JWT claim present in virtually all tokens - Updates every time a new token is issued (including refreshes) - Different from auth_time which only updates on actual authentication ## Steps Step 1: Ingest Graph Data - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: POST Person, PaymentMethod, Company nodes and relationships - Result: Graph ready for operations Step 2: Create Policy with iat Freshness Check - Authentication: ServiceAccount credential (Bearer token) - Action: POST policy with conditions: - Calculate token age from iat claim - If token age > 3600 seconds (1 hour): Return refresh advice - If token age <= 3600 seconds: Allow operation - Result: Policy ID returned Step 3: Create Query with Token Freshness Requirement - Authentication: ServiceAccount credential (Bearer token) - Action: POST query that will check token freshness before executing - Result: Query ID returned Step 4: Execute with Old Token - Authentication: AppAgent credential + User token (with old iat) - Action: POST to /contx-iq/v1/execute - Result if iat too old: {decision: false, advice: {refresh_token: true}} - Result if iat recent: Operation succeeds ## Code Examples ### Step 1 Capture the nodes needed for this use case. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "alice", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "alice@email.com" }, { "type": "given_name", "value": "Alice" }, { "type": "last_name", "value": "Smith" } ] }, { "external_id": "bob", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "bob@email.com" }, { "type": "given_name", "value": "Bob" } ] }, { "external_id": "cb123", "type": "PaymentMethod", "properties": [ { "type": "payment_name", "value": "Credit Card" } ] }, { "external_id": "kl123", "type": "PaymentMethod", "properties": [ { "type": "payment_name", "value": "Klarna" } ] }, { "external_id": "ct123", "type": "Contract", "properties": [ { "type": "category", "value": "Parking" }, { "type": "status", "value": "Active" } ] }, { "external_id": "ct234", "type": "Contract", "properties": [ { "type": "category", "value": "Parking" }, { "type": "status", "value": "Active" } ] }, { "external_id": "car1", "type": "Vehicle", "properties": [ { "type": "category", "value": "Car" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "rtfhcnvjt471" } ] }, { "external_id": "car2", "type": "Vehicle", "properties": [ { "type": "category", "value": "Car" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "kdcbfrt178" } ] }, { "external_id": "license1", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "AX123456", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "license2", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "OL123456", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "companyParking", "type": "Company", "properties": [ { "type": "name", "value": "City Parking Inc" } ] } ] } ``` Capture the relationships needed for this use case. **POST https://eu.api.indykite.com/capture/v1/relationships/** ```json { "relationships": [ { "source": { "external_id": "bob", "type": "Person" }, "target": { "external_id": "kl123", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "cb123", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "ct123", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "bob", "type": "Person" }, "target": { "external_id": "ct234", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "ct123", "type": "Contract" }, "target": { "external_id": "car1", "type": "Vehicle" }, "type": "COVERS" }, { "source": { "external_id": "ct234", "type": "Contract" }, "target": { "external_id": "car2", "type": "Vehicle" }, "type": "COVERS" }, { "source": { "external_id": "car1", "type": "Vehicle" }, "target": { "external_id": "license1", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "car2", "type": "Vehicle" }, "target": { "external_id": "license2", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "companyParking", "type": "Company" }, "target": { "external_id": "ct234", "type": "Contract" }, "type": "OFFERS" }, { "source": { "external_id": "companyParking", "type": "Company" }, "target": { "external_id": "ct123", "type": "Contract" }, "type": "OFFERS" } ] } ``` ### Step 2 Create a CIQ Policy which designates an advice step up if the access token iat claim is more than an hour ago. **policy.json** ```json { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "Person" }, "condition": { "cypher": "MATCH (company:Company)-[:OFFERS]->(contract:Contract)<-[:ACCEPTED]-(subject:Person)-[:HAS]->(payment:PaymentMethod), (contract)-[:COVERS]->(vehicle:Vehicle)-[:HAS]->(ln:LicenseNumber)", "filter": [ { "operator": "AND", "operands": [ { "attribute": "subject.external_id", "operator": "=", "value": "$subject_external_id" }, { "attribute": "$token.sub", "operator": "=", "value": "$token_sub" } ] } ], "token_filter": { "operator": ">=", "attribute": "$token.iat", "value": "$one_hour_ago", "advice": { "error": "insufficient_user_authentication", "error_description": "More recent authentication is required, token > 1 hour ago" } } }, "allowed_reads": { "nodes": [ "company.*", "subject.*", "payment.*" ] } } ``` Request to create the CIQ Policy configuration using REST. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"meta\":{\"policy_version\":\"1.0-ciq\"},\"subject\":{\"type\":\"Person\"},\"condition\":{\"cypher\":\"MATCH (company:Company)-[:OFFERS]->(contract:Contract)<-[:ACCEPTED]-(subject:Person)-[:HAS]->(payment:PaymentMethod), (contract)-[:COVERS]->(vehicle:Vehicle)-[:HAS]->(ln:LicenseNumber)\",\"filter\":[{\"operator\":\"AND\",\"operands\":[{\"attribute\":\"subject.external_id\",\"operator\":\"=\",\"value\":\"$subject_external_id\"},{\"attribute\":\"$token.sub\",\"operator\":\"=\",\"value\":\"$token_sub\"}]}],\"token_filter\":{\"operator\":\">=\",\"attribute\":\"$token.iat\",\"value\":\"$one_hour_ago\",\"advice\":{\"error\":\"insufficient_user_authentication\",\"error_description\":\"More recent authentication is required, token > 1 hour ago\"}}},\"allowed_reads\":{\"nodes\":[\"company.*\",\"subject.*\",\"payment.*\"]}}", "status": "ACTIVE", "tags": [] } ``` Request to read the CIQ Policy configuration using REST. **policy_request.json** ```json { "id": "your_policy_configuration_gid" } ``` ### Step 3 Create a CIQ Query in the context of the policy to configure the step up advice. **knowledge_query.json** ```json { "nodes": [ "company.external_id", "payment.external_id" ] } ``` Request to create a CIQ Query configuration using REST. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "description of knowledge query", "display_name": "knowledge query name", "name": "knowledge-query-name", "policy_id": "your_policy_gid", "query": "{\"nodes\":[\"company.external_id\",\"payment.external_id\"]}", "status": "ACTIVE" } ``` Read the CIQ Query Configuration. **GET https://eu.api.indykite.com/configs/v1/knowledge-queries/{id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 4 Run a CIQ Execution to trigger the step up advice. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "knowledge_query_gid", "input_params": { "subject_external_id": "alice", "token_sub": "alice_user_external_id", "one_hour_ago": 1789567988 }, "page_token": 1 } ``` CIQ Execution response if the iat is too old. **response.json** ```json { "message": "Unauthorized: invalid token. see response Www-Authenticate headers" } ``` CIQ Execution response if the iat is less than an hour ago. **response.json** ```json { "data": [ { "nodes": { "company.external_id": "companyParking", "payment.external_id": "cb123" } } ] } ``` ## Common Errors ### 401: UNAUTHENTICATED **Solution:** Check credentials: ServiceAccount Bearer token for config APIs, X-IK-ClientKey for execution ### 404: NOT_FOUND **Solution:** Verify the query/policy ID exists and belongs to your project --- Source: https://developer.indykite.com/resources/ciq-13 --- # ContX IQ: Query External Data Sources via Data Resolver > Demonstrates fetching data from external systems (APIs, databases) during ContX IQ query execution. The External Data Resolver retrieves information not stored in the IKG, combining graph data with real-time external lookups. **Category:** ContX IQ **API:** ContX IQ **Tags:** ContX IQ Policy, ContX IQ Query, External Data, Data Resolver, API Integration, VIN Lookup, Hybrid Data **Last Updated:** 2026-03-20 **OpenAPI Endpoints:** /capture/v1/nodes, /capture/v1/relationships, /configs/v1/authorization-policies, /configs/v1/knowledge-queries, /contx-iq/v1/execute **Related Guides:** /guides/guide-contx-iq, /guides/guide-sandbox ## Summary This example demonstrates External Data Resolver integration: What is an External Data Resolver? A configuration that fetches data from external systems (APIs, databases) during query execution. Use case: Vehicle VIN numbers are stored in an external system, not the IKG. Flow: 1. Query requests vehicle.vin property 2. IKG has Vehicle node but no VIN stored 3. External Data Resolver fetches VIN from external API 4. Query returns combined IKG + external data Benefits: - Keep sensitive data in external systems - Real-time data lookups - Reduce data duplication - Maintain single source of truth ## Use Case Scenario: A car rental app needs to display vehicle VIN numbers, but VINs are stored in a separate vehicle registry system. Graph in IKG: Person(Alice) -[DRIVES]-> Vehicle(Car1) Vehicle(Car1) has: category="SUV", data_ref="ext://vehicles/car1" External Vehicle Registry: car1: {vin: "1HGBH41JXMN109186"} Query execution: 1. User (Alice) requests vehicle details 2. Policy authorizes: Alice can READ vehicles she DRIVES 3. Query fetches category from IKG: "SUV" 4. Query detects data_ref, calls External Data Resolver 5. Resolver fetches VIN from external registry 6. Response: {category: "SUV", vin: "1HGBH41JXMN109186"} The VIN never needs to be stored in the IKG - it's fetched in real-time. ## Requirements Prerequisites: - ServiceAccount credentials: For configuration and policy creation (Bearer token) - AppAgent credentials: For data ingestion and query execution (X-IK-ClientKey) - User access token: For authorized user (Alice) - External API access: The resolver endpoint must be accessible External Data Resolver setup: - Configured via POST /configs/v1/external-data-resolvers - Defines endpoint URL, authentication, and data mapping - In this example, returns mock data: vin="vinmagic" ## Steps Step 1: Create External Data Resolver Configuration - Authentication: ServiceAccount credential (Bearer token) - Action: POST to /configs/v1/external-data-resolvers - Configuration includes: - Resolver endpoint URL - Authentication method - Data mapping (how to extract VIN from response) - Result: Resolver ID returned Step 2: Ingest Nodes with Data References - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: POST Person, Vehicle nodes - Key detail: Vehicle node includes data_ref property pointing to external data - Format: data_ref: "ext://resolver-id/path" - Result: Graph ready with external data references Step 3: Create Policy for External Data Access - Authentication: ServiceAccount credential (Bearer token) - Action: POST policy allowing: - READ on Vehicle nodes through Person -[DRIVES]-> Vehicle path - ACCESS to external data via data_ref - Result: Policy ID returned Step 4: Create Query Returning External Data - Authentication: ServiceAccount credential (Bearer token) - Action: POST query that returns vehicle.category (IKG) and vehicle.vin (external) - The query engine automatically resolves data_ref properties - Result: Query ID returned Step 5: Execute Query as Authorized User - Authentication: AppAgent credential + User token (Bearer header) - Action: POST to /contx-iq/v1/execute - Result: Combined response with IKG data + external VIN ## Code Examples ### Step 1 Create an External Data Resolver which configures the data to retrieve. In this use case, the configuration always returns "vinmagic" as a vin value. **POST https://eu.api.indykite.com/configs/v1/external-data-resolvers** ```json { "project_id": "your_project_gid", "description": "description of external data reference", "display_name": "external data name", "name": "externalDataResolverForCIQName", "headers": {}, "method": "GET", "request_content_type": "JSON", "request_payload": "", "response_content_type": "JSON", "response_selector": ".echo", "url": "http://whateverUrlWithExternalValue/magic?data=vinmagic" } ``` ### Step 2 Capture the nodes needed for this use case. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "alice", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "alice@email.com" }, { "type": "given_name", "value": "Alice" }, { "type": "last_name", "value": "Smith" } ] }, { "external_id": "bob", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "bob@email.com" }, { "type": "given_name", "value": "Bob" } ] }, { "external_id": "cb123", "type": "PaymentMethod", "properties": [ { "type": "payment_name", "value": "Credit Card" } ] }, { "external_id": "kl123", "type": "PaymentMethod", "properties": [ { "type": "payment_name", "value": "Klarna" } ] }, { "external_id": "ct123", "type": "Contract", "properties": [ { "type": "category", "value": "Parking" }, { "type": "status", "value": "Active" } ] }, { "external_id": "ct234", "type": "Contract", "properties": [ { "type": "category", "value": "Parking" }, { "type": "status", "value": "Active" } ] }, { "external_id": "car1", "type": "Vehicle", "properties": [ { "type": "category", "value": "Car" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "rtfhcnvjt471" } ] }, { "external_id": "car2", "type": "Vehicle", "properties": [ { "type": "category", "value": "Car" }, { "type": "is_active", "value": true }, { "type": "vin", "external_value": "externalDataResolverForCIQName" } ] }, { "external_id": "license1", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "AX123456", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "license2", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "OL123456", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "companyParking", "type": "Company", "properties": [ { "type": "name", "value": "City Parking Inc" } ] } ] } ``` Capture the relationships needed for this use case. **POST https://eu.api.indykite.com/capture/v1/relationships/** ```json { "relationships": [ { "source": { "external_id": "bob", "type": "Person" }, "target": { "external_id": "kl123", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "cb123", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "ct123", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "bob", "type": "Person" }, "target": { "external_id": "ct234", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "ct123", "type": "Contract" }, "target": { "external_id": "car1", "type": "Vehicle" }, "type": "COVERS" }, { "source": { "external_id": "ct234", "type": "Contract" }, "target": { "external_id": "car2", "type": "Vehicle" }, "type": "COVERS" }, { "source": { "external_id": "car1", "type": "Vehicle" }, "target": { "external_id": "license1", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "car2", "type": "Vehicle" }, "target": { "external_id": "license2", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "companyParking", "type": "Company" }, "target": { "external_id": "ct234", "type": "Contract" }, "type": "OFFERS" }, { "source": { "external_id": "companyParking", "type": "Company" }, "target": { "external_id": "ct123", "type": "Contract" }, "type": "OFFERS" } ] } ``` ### Step 3 Create a CIQ Policy which designates the data to retrieve. **policy.json** ```json { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "Person" }, "condition": { "cypher": "MATCH (company:Company)-[:OFFERS]->(contract:Contract)<-[:ACCEPTED]-(subject:Person)-[:HAS]->(payment:PaymentMethod), (contract)-[:COVERS]->(vehicle:Vehicle)-[:HAS]->(ln:LicenseNumber)", "filter": [ { "operator": "AND", "operands": [ { "attribute": "subject.external_id", "operator": "=", "value": "$subject_external_id" }, { "attribute": "$token.sub", "operator": "=", "value": "$token_sub" } ] } ] }, "allowed_reads": { "nodes": [ "vehicle", "ln", "vehicle.*", "ln.*" ] } } ``` Request to create the CIQ Policy configuration using REST. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"meta\":{\"policy_version\":\"1.0-ciq\"},\"subject\":{\"type\":\"Person\"},\"condition\":{\"cypher\":\"MATCH (company:Company)-[:OFFERS]->(contract:Contract)<-[:ACCEPTED]-(subject:Person)-[:HAS]->(payment:PaymentMethod), (contract)-[:COVERS]->(vehicle:Vehicle)-[:HAS]->(ln:LicenseNumber)\",\"filter\":[{\"operator\":\"AND\",\"operands\":[{\"attribute\":\"subject.external_id\",\"operator\":\"=\",\"value\":\"$subject_external_id\"},{\"attribute\":\"$token.sub\",\"operator\":\"=\",\"value\":\"$token_sub\"}]}]},\"allowed_reads\":{\"nodes\":[\"vehicle\",\"ln\",\"vehicle.*\",\"ln.*\"]}}", "status": "ACTIVE", "tags": [] } ``` Request to read the CIQ Policy configuration using REST. **policy_request.json** ```json { "id": "your_policy_configuration_gid" } ``` ### Step 4 Create a CIQ Query in the context of the policy to retrieve the vehicle category and the vehicle vin. **knowledge_query.json** ```json { "nodes": [ "vehicle", "vehicle.property.category", "vehicle.property.vin" ] } ``` Request to create a CIQ Query configuration using REST. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "description of knowledge query", "display_name": "knowledge query name", "name": "knowledge-query-name", "policy_id": "your_policy_gid", "query": "{\"nodes\":[\"vehicle\",\"vehicle.property.category\",\"vehicle.property.vin\"]}", "status": "ACTIVE" } ``` Read the CIQ Query Configuration. **GET https://eu.api.indykite.com/configs/v1/knowledge-queries/{id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 5 Run a CIQ Execution to actually retrieve the data, from the Person access token. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "knowledge_query_gid", "input_params": { "subject_external_id": "alice", "token_sub": "alice_user_external_id" }, "page_token": 1 } ``` CIQ Execution response. **response.json** ```json { "data": [ { "nodes": { "vehicle": { "Id": 9, "ElementId": "4:a5c213aa-aa4b-4be5-a17a-a677a80ee634:9", "Labels": [ "Unique", "Resource", "Vehicle" ], "Props": { "_service": "capture-api", "create_time": "2025-07-28T13:47:58.062Z", "external_id": "car2", "id": "sF_yDttqSKi_oUwiv7OSww", "type": "Vehicle", "update_time": "2025-07-28T13:47:58.062Z" } }, "vehicle.property.category": "Car", "vehicle.property.vin": "vinmagic" } } ] } ``` ## Common Errors ### 401: UNAUTHENTICATED **Solution:** Check credentials: ServiceAccount Bearer token for config APIs, X-IK-ClientKey for execution ### 404: NOT_FOUND **Solution:** Verify the query/policy ID exists and belongs to your project --- Source: https://developer.indykite.com/resources/ciq-14 --- # ContX IQ: Manage Node Property Metadata > Add, update, and query metadata on node properties. Metadata provides additional context like timestamps, sources, confidence scores, or audit information for individual property values. **Category:** ContX IQ **API:** ContX IQ **Tags:** ContX IQ Policy, ContX IQ Query, Metadata, Property Attributes, Data Provenance, Audit Trail **Last Updated:** 2026-03-20 **OpenAPI Endpoints:** /capture/v1/nodes, /capture/v1/relationships, /configs/v1/authorization-policies, /configs/v1/knowledge-queries, /contx-iq/v1/execute **Related Guides:** /guides/guide-contx-iq, /guides/guide-sandbox ## Summary This example demonstrates property-level metadata management: What is property metadata? Additional attributes attached to individual property values, not the node itself. Example: Node: Person(name: "Alice") Property metadata for "name": - source: "HR_System" - confidence: 0.95 - last_verified: "2024-01-15" Operations demonstrated: 1. Add metadata to existing property 2. Create new property with metadata 3. Query/filter by metadata values ## Use Case Scenario: Track data provenance and quality scores for identity attributes. Initial state: Person(email: "alice@example.com") After metadata upsert: Person(email: "alice@example.com") └── metadata: - source: "email_verification_service" - verified: true - verification_date: "2024-01-15" Use cases for metadata: - Data lineage: Track where data came from - Quality scores: Confidence levels for ML-derived data - Audit trail: When was data last verified - Compliance: GDPR-related tracking Query by metadata: "Find all Person nodes where email.verified = true" ## Requirements Prerequisites: - ServiceAccount credentials: For creating policies and queries (Bearer token) - AppAgent credentials: For data ingestion and query execution (X-IK-ClientKey) - User access token: For authorized user operations Metadata structure: - Attached to individual properties, not nodes - Key-value pairs with any JSON-compatible values - Queryable/filterable in ContX IQ ## Steps Step 1: Ingest Base Graph Data - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: POST nodes and relationships - Result: Base graph ready for metadata operations Step 2: Create Metadata Upsert Policy - Authentication: ServiceAccount credential (Bearer token) - Action: POST policy allowing: - READ on subject nodes - UPSERT on property metadata - READ on nodes with metadata - Result: Policy ID returned Step 3: Create Metadata Upsert Query - Authentication: ServiceAccount credential (Bearer token) - Action: POST query that: - Adds metadata to existing property (e.g., email.source) - Creates new property with metadata - Result: Query ID returned Step 4: Execute Metadata Upsert - Authentication: AppAgent credential + User token - Action: POST to /contx-iq/v1/execute - Result: Properties now have metadata attached Step 5: Create Metadata Filter Query - Authentication: ServiceAccount credential (Bearer token) - Action: POST query that filters by metadata values - Example: Find nodes where property.metadata.source = "HR_System" - Result: Query ID returned Step 6: Execute Metadata Filter Query - Authentication: AppAgent credential + User token - Action: POST to /contx-iq/v1/execute - Result: Only nodes matching metadata filter returned ## Code Examples ### Step 1 Capture the nodes needed for this use case. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "alice", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "alice@email.com" }, { "type": "name", "value": "Alice Smith" } ] }, { "external_id": "satchmo", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "satchmo@demo.com" }, { "type": "name", "value": "Louis Armstrong" } ] }, { "external_id": "karel", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "karel@demo.com" }, { "type": "name", "value": "Karel Plihal" } ] }, { "external_id": "kitt", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "pontiac" }, { "type": "model", "value": "Firebird" } ] }, { "external_id": "caddilacv16", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "Caddilac" }, { "type": "model", "value": "V-16" } ] }, { "external_id": "harmonika", "type": "Bus", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "Ikarus" }, { "type": "model", "value": "280" } ] }, { "external_id": "ln-xxx", "type": "LicenseNumber", "is_identity": false, "properties": [ { "type": "license", "value": "ln-xxx-value" } ] }, { "external_id": "ln-yyy", "type": "LicenseNumber", "is_identity": false, "properties": [ { "type": "license", "value": "ln-yyy-value" } ] }, { "external_id": "ln-zzz", "type": "LicenseNumber", "is_identity": false, "properties": [ { "type": "license", "value": "ln-zzz-value" } ] } ] } ``` Capture the relationships needed for this use case. **POST https://eu.api.indykite.com/capture/v1/relationships/** ```json { "relationships": [ { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "kitt", "type": "Car" }, "type": "OWNS", "properties": [ { "type": "weight", "value": 1 } ] }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "caddilacv16", "type": "Car" }, "type": "OWNS", "properties": [ { "type": "weight", "value": 2 } ] }, { "source": { "external_id": "satchmo", "type": "Person" }, "target": { "external_id": "caddilacv16", "type": "Car" }, "type": "OWNS" }, { "source": { "external_id": "karel", "type": "Person" }, "target": { "external_id": "harmonika", "type": "Bus" }, "type": "OWNS" }, { "source": { "external_id": "kitt", "type": "Car" }, "target": { "external_id": "ln-xxx", "type": "LicenseNumber" }, "type": "HAS", "properties": [ { "type": "weight", "value": 3 } ] }, { "source": { "external_id": "caddilacv16", "type": "Car" }, "target": { "external_id": "ln-zzz", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "harmonika", "type": "Bus" }, "target": { "external_id": "ln-yyy", "type": "LicenseNumber" }, "type": "HAS" } ] } ``` ### Step 2 Create a CIQ Policy which designates the Subject node, the cypher, the nodes allowed to be upserted and the nodes allowed to be read. **policy.json** ```json { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "Person" }, "condition": { "cypher": "MATCH (subject:Person)-[:OWNS]->(car:Car)-[:HAS]->(ln:LicenseNumber)", "filter": [ { "operator": "AND", "operands": [ { "attribute": "subject.external_id", "operator": "=", "value": "$subject_external_id" }, { "attribute": "$token.sub", "operator": "=", "value": "$token_sub" } ] } ] }, "allowed_reads": { "nodes": [ "car", "ln", "car.*", "ln.*" ] }, "allowed_upserts": { "nodes": { "existing_nodes": [ "ln" ] } } } ``` Request to create the CIQ Policy configuration using REST. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"meta\":{\"policy_version\":\"1.0-ciq\"},\"subject\":{\"type\":\"Person\"},\"condition\":{\"cypher\":\"MATCH (subject:Person)-[:OWNS]->(car:Car)-[:HAS]->(ln:LicenseNumber)\",\"filter\":[{\"operator\":\"AND\",\"operands\":[{\"attribute\":\"subject.external_id\",\"operator\":\"=\",\"value\":\"$subject_external_id\"},{\"attribute\":\"$token.sub\",\"operator\":\"=\",\"value\":\"$token_sub\"}]}]},\"allowed_reads\":{\"nodes\":[\"car\",\"ln\",\"car.*\",\"ln.*\"]},\"allowed_upserts\":{\"nodes\":{\"existing_nodes\":[\"ln\"]}}}", "status": "ACTIVE", "tags": [] } ``` Request to read the CIQ Policy configuration using REST. **policy_request.json** ```json { "id": "your_policy_configuration_gid" } ``` ### Step 3 Create a CIQ Query in the context of the policy to upsert and retrieve properties with metadata on a node. **knowledge_query.json** ```json { "nodes": [ "car", "ln.property.status", "ln.property.status.metadata.source", "ln.property.status.metadata.assurance_level", "ln.property.status.metadata.somethingImportant", "ln.property.license", "ln.property.license.metadata.source" ], "relationships": [], "upsert_nodes": [ { "name": "ln", "properties": [ { "type": "status", "value": "$status", "metadata": [ { "type": "source", "value": "$token.iss" }, { "type": "assurance_level", "value": 2 }, { "type": "somethingImportant", "value": "supercoolvalue" } ] }, { "type": "license", "value": "$ln_number", "metadata": [ { "type": "source", "value": "The government" } ] } ] } ], "filter": { "attribute": "ln.external_id", "operator": "=", "value": "$ln_external_id" } } ``` Request to create a CIQ Query configuration using REST. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "description of knowledge query", "display_name": "knowledge query name", "name": "knowledge-query-name", "policy_id": "your_policy_gid", "query": "{\"nodes\":[\"car\",\"ln.property.status\",\"ln.property.status.metadata.source\",\"ln.property.status.metadata.assurance_level\",\"ln.property.status.metadata.somethingImportant\",\"ln.property.license\",\"ln.property.license.metadata.source\"],\"relationships\":[],\"upsert_nodes\":[{\"name\":\"ln\",\"properties\":[{\"type\":\"status\",\"value\":\"$status\",\"metadata\":[{\"type\":\"source\",\"value\":\"$token.iss\"},{\"type\":\"assurance_level\",\"value\":2},{\"type\":\"somethingImportant\",\"value\":\"supercoolvalue\"}]},{\"type\":\"license\",\"value\":\"$ln_number\",\"metadata\":[{\"type\":\"source\",\"value\":\"The government\"}]}]}],\"filter\":{\"attribute\":\"ln.external_id\",\"operator\":\"=\",\"value\":\"$ln_external_id\"}}", "status": "ACTIVE" } ``` Read the CIQ Query Configuration. **GET https://eu.api.indykite.com/configs/v1/knowledge-queries/{id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 4 Run a CIQ Execution to get the newly created property information. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "knowledge_query_gid", "input_params": { "subject_external_id": "alice", "token_sub": "alice_user_external_id", "status": "Valid", "ln_number": "ln-xxx-value", "ln_external_id": "ln-xxx" }, "page_token": 1 } ``` CIQ Execution response. **response.json** ```json { "data": [ { "nodes": { "car": { "Id": 58, "ElementId": "4:a5c213aa-aa4b-4be5-a17a-a677a80ee634:58", "Labels": [ "Unique", "Resource", "Car" ], "Props": { "_service": "capture-api", "create_time": "2025-08-13T17:19:43.014Z", "external_id": "kitt", "id": "twixV9zqQniA201VtZdzxw", "type": "Car", "update_time": "2025-08-13T17:20:00.141Z" } }, "ln.property.license": "ln-xxx-value", "ln.property.license.metadata.source": "The government", "ln.property.status": "Valid", "ln.property.status.metadata.assurance_level": 2, "ln.property.status.metadata.somethingImportant": "supercoolvalue", "ln.property.status.metadata.source": "https://issuer_url" } } ] } ``` ### Step 5 Create a CIQ Query in the context of the policy to retrieve property information according to a filter on one of the newly created metadata. **knowledge_query.json** ```json { "nodes": [ "car", "ln.property.status", "ln.property.status.metadata.source", "ln.property.status.metadata.assurance_level" ], "relationships": [], "filter": { "attribute": "ln.property.status.metadata.assurance_level", "operator": ">=", "value": 2 } } ``` Request to create a CIQ Query configuration using REST. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "description of knowledge query", "display_name": "knowledge query name", "name": "knowledge-query-name", "policy_id": "your_policy_gid", "query": "{\"nodes\":[\"car\",\"ln.property.status\",\"ln.property.status.metadata.source\",\"ln.property.status.metadata.assurance_level\"],\"relationships\":[],\"filter\":{\"attribute\":\"ln.property.status.metadata.assurance_level\",\"operator\":\">=\",\"value\":2}}", "status": "ACTIVE" } ``` Read the CIQ Query Configuration. **GET https://eu.api.indykite.com/configs/v1/knowledge-queries/{id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 6 Run a CIQ Execution to read the data. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "knowledge_query_gid", "input_params": { "subject_external_id": "alice", "token_sub": "alice_user_external_id" }, "page_token": 1 } ``` CIQ Execution response. **response.json** ```json { "data": [ { "nodes": { "car": { "Id": 16, "ElementId": "4:a5c213aa-aa4b-4be5-a17a-a677a80ee634:16", "Labels": [ "Unique", "Resource", "Car" ], "Props": { "_service": "capture-api", "create_time": "2025-08-13T14:18:57.565Z", "external_id": "kitt", "id": "ylK0vV4_T2CeCUxpolR7aw", "type": "Car", "update_time": "2025-08-13T14:18:57.565Z" } }, "ln.property.status": "Valid", "ln.property.status.metadata.assurance_level": 2, "ln.property.status.metadata.source": "https://issuer_url" } } ] } ``` ## Common Errors ### 401: UNAUTHENTICATED **Solution:** Check credentials: ServiceAccount Bearer token for config APIs, X-IK-ClientKey for execution ### 404: NOT_FOUND **Solution:** Verify the query/policy ID exists and belongs to your project --- Source: https://developer.indykite.com/resources/ciq-15 --- # ContX IQ: Delete Node Properties and Relationship Properties > Demonstrates granular delete operations: delete entire nodes, specific properties from nodes, and specific properties from relationships. Useful for data cleanup, GDPR right-to-erasure, and selective data removal. **Category:** ContX IQ **API:** ContX IQ **Tags:** ContX IQ Policy, ContX IQ Query, Delete Properties, Property Removal, Data Cleanup, GDPR Erasure **Last Updated:** 2026-03-20 **OpenAPI Endpoints:** /capture/v1/nodes, /capture/v1/relationships, /configs/v1/authorization-policies, /configs/v1/knowledge-queries, /contx-iq/v1/execute **Related Guides:** /guides/guide-contx-iq, /guides/guide-sandbox ## Summary This example demonstrates fine-grained delete operations: Delete types supported: 1. Delete entire nodes (and their relationships) 2. Delete specific properties from nodes (node remains) 3. Delete specific properties from relationships (relationship remains) Example operations: - Delete LicenseNumber nodes entirely - Delete Car.model and Car.vin properties (Car nodes remain) - Delete OWNS.weight property (OWNS relationship remains) Use cases: - GDPR right-to-erasure (selective data removal) - Data cleanup and normalization - Removing deprecated properties ## Use Case Scenario: Remove sensitive/deprecated data while preserving graph structure. Initial state: Person(Alice) -[OWNS {weight: "100kg"}]-> Car(model: "Mustang", vin: "ABC123") Car -[HAS]-> LicenseNumber("ABC-123") Delete operations: 1. Delete LicenseNumber nodes (and HAS relationships) 2. Delete Car.model and Car.vin properties 3. Delete OWNS.weight property Final state: Person(Alice) -[OWNS]-> Car() - LicenseNumber nodes gone - Car nodes exist but without model/vin properties - OWNS relationship exists but without weight property Query returns IDs of all modified nodes for confirmation. ## Requirements Prerequisites: - ServiceAccount credentials: For creating policies and queries (Bearer token) - AppAgent credentials: For data ingestion and query execution (X-IK-ClientKey) - User access token: For authorized delete operations Delete authorization: - Policy specifies allowed_deletes.nodes and allowed_deletes.properties - User must be authorized for each type of delete operation ## Steps Step 1: Ingest Graph with Properties to Delete - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: POST nodes with properties (model, vin, etc.) and relationships with properties (weight) - Result: Graph ready for delete operations Step 2: Create Delete Policy - Authentication: ServiceAccount credential (Bearer token) - Action: POST policy with: - allowed_deletes.nodes: [LicenseNumber] - allowed_deletes.node_properties: [{label: Car, properties: [model, vin]}] - allowed_deletes.relationship_properties: [{type: OWNS, properties: [weight]}] - Result: Policy ID returned Step 3: Create Delete Query - Authentication: ServiceAccount credential (Bearer token) - Action: POST query that: - Matches and deletes LicenseNumber nodes - Removes model, vin properties from Car nodes - Removes weight property from OWNS relationships - Returns external_ids of modified nodes - Result: Query ID returned Step 4: Execute Delete Operations - Authentication: AppAgent credential + User token - Action: POST to /contx-iq/v1/execute - Result: Returns list of modified node IDs; properties/nodes deleted from graph ## Code Examples ### Step 1 Capture the nodes needed for this use case. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "alice", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "alice@email.com" }, { "type": "name", "value": "Alice Smith" } ] }, { "external_id": "satchmo", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "satchmo@demo.com" }, { "type": "name", "value": "Louis Armstrong" } ] }, { "external_id": "karel", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "karel@demo.com" }, { "type": "name", "value": "Karel Plihal" } ] }, { "external_id": "kitt", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "pontiac" }, { "type": "model", "value": "Firebird" } ] }, { "external_id": "caddilacv16", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "Caddilac" }, { "type": "model", "value": "V-16" } ] }, { "external_id": "harmonika", "type": "Bus", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "Ikarus" }, { "type": "model", "value": "280" } ] }, { "external_id": "ln-xxx", "type": "LicenseNumber", "is_identity": false, "properties": [ { "type": "license", "value": "ln-xxx-value" } ] }, { "external_id": "ln-yyy", "type": "LicenseNumber", "is_identity": false, "properties": [ { "type": "license", "value": "ln-yyy-value" } ] }, { "external_id": "ln-zzz", "type": "LicenseNumber", "is_identity": false, "properties": [ { "type": "license", "value": "ln-zzz-value" } ] } ] } ``` Capture the relationships needed for this use case. **POST https://eu.api.indykite.com/capture/v1/relationships/** ```json { "relationships": [ { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "kitt", "type": "Car" }, "type": "OWNS", "properties": [ { "type": "weight", "value": 1 } ] }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "caddilacv16", "type": "Car" }, "type": "OWNS", "properties": [ { "type": "weight", "value": 2 } ] }, { "source": { "external_id": "satchmo", "type": "Person" }, "target": { "external_id": "caddilacv16", "type": "Car" }, "type": "OWNS" }, { "source": { "external_id": "karel", "type": "Person" }, "target": { "external_id": "harmonika", "type": "Bus" }, "type": "OWNS" }, { "source": { "external_id": "kitt", "type": "Car" }, "target": { "external_id": "ln-xxx", "type": "LicenseNumber" }, "type": "HAS", "properties": [ { "type": "weight", "value": 3 } ] }, { "source": { "external_id": "caddilacv16", "type": "Car" }, "target": { "external_id": "ln-zzz", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "harmonika", "type": "Bus" }, "target": { "external_id": "ln-yyy", "type": "LicenseNumber" }, "type": "HAS" } ] } ``` ### Step 2 Create a CIQ Policy which designates the Subject node, the cypher, the nodes allowed to be deleted and the nodes allowed to be read. **policy.json** ```json { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "Person" }, "condition": { "cypher": "MATCH (subject:Person)-[r1:OWNS]->(car:Car)-[r2:HAS]->(ln:LicenseNumber)", "filter": [ { "app": "postman", "operator": "=", "attribute": "subject.external_id", "value": "$subject_external_id" } ] }, "allowed_reads": { "nodes": [ "subject.*", "ln.property.value" ], "relationships": [ "r1.*", "r2.*" ] }, "allowed_deletes": { "nodes": [ "ln", "ln.*", "car.*" ], "relationships": [ "r1.*", "r1", "r2" ] } } ``` Request to create the CIQ Policy configuration using REST. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"meta\":{\"policy_version\":\"1.0-ciq\"},\"subject\":{\"type\":\"Person\"},\"condition\":{\"cypher\":\"MATCH (subject:Person)-[r1:OWNS]->(car:Car)-[r2:HAS]->(ln:LicenseNumber)\",\"filter\":[{\"app\":\"postman\",\"operator\":\"=\",\"attribute\":\"subject.external_id\",\"value\":\"$subject_external_id\"}]},\"allowed_reads\":{\"nodes\":[\"subject.*\",\"ln.property.value\"],\"relationships\":[\"r1.*\",\"r2.*\"]},\"allowed_deletes\":{\"nodes\":[\"ln\",\"ln.*\",\"car.*\"],\"relationships\":[\"r1.*\",\"r1\",\"r2\"]}}", "status": "ACTIVE", "tags": [] } ``` Request to read the CIQ Policy configuration using REST. **policy_request.json** ```json { "id": "your_policy_configuration_gid" } ``` ### Step 3 Create a CIQ Knwledge Query in the context of the policy to delete License Number nodes, delete the model and vin properties of Car nodes, and delete the weight properties of OWNS relationships of the authorized subject. **knowledge_query.json** ```json { "nodes": [ "subject.external_id" ], "delete_nodes": [ "ln", "car.property.model", "car.property.vin" ], "delete_relationships": [ "r1.weight" ] } ``` Request to create a CIQ Knowledge Query configuration using REST. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "description of knowledge query", "display_name": "knowledge query name", "name": "knowledge-query-name", "policy_id": "your_policy_gid", "query": "{\"nodes\":[\"subject.external_id\"],\"delete_nodes\":[\"ln\",\"car.property.model\",\"car.property.vin\"],\"delete_relationships\":[\"r1.weight\"]}", "status": "ACTIVE" } ``` Read the CIQ Query Configuration. **GET https://eu.api.indykite.com/configs/v1/knowledge-queries/{id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 4 Run a CIQ query to identify and return the external_id of all nodes that have had properties removed. Also, identify nodes that are connected to relationships from which properties have been deleted. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "knowledge_query_gid", "input_params": { "subject_external_id": "alice" }, "page_token": 1 } ``` CIQ Execution response. **response.json** ```json { "data": [ { "nodes": { "subject.external_id": "alice" } }, { "nodes": { "subject.external_id": "alice" } } ] } ``` ## Common Errors ### 401: UNAUTHENTICATED **Solution:** Check credentials: ServiceAccount Bearer token for config APIs, X-IK-ClientKey for execution ### 404: NOT_FOUND **Solution:** Verify the query/policy ID exists and belongs to your project --- Source: https://developer.indykite.com/resources/ciq-16 --- # ContX IQ: Application as Service - Create Person Linked to Multiple Nodes > Demonstrates service-to-service data creation where an Application (not a user) creates new Person nodes and links them to existing Country and Company nodes. Common pattern for system integrations and automated data pipelines. **Category:** ContX IQ **API:** ContX IQ **Tags:** ContX IQ Policy, ContX IQ Query, Service Account, _Application Subject, Node Creation, Multi-Relationship **Last Updated:** 2026-03-20 **OpenAPI Endpoints:** /capture/v1/nodes, /capture/v1/relationships, /configs/v1/authorization-policies, /configs/v1/knowledge-queries, /contx-iq/v1/execute **Related Guides:** /guides/guide-contx-iq, /guides/guide-sandbox ## Summary This example shows Application-as-subject creating nodes with multiple relationships: Service-to-service pattern: - No user token involved - Application authenticates with its API key - Application creates data on behalf of the system Operation: Create Person node and link to two existing nodes: - Person -[LIVES_IN]-> Country - Person -[WORKS_AT]-> Company Use cases: - Data import pipelines - System integrations - Automated onboarding workflows ## Use Case Scenario: An HR integration system automatically creates employee records. Existing data: - Country(USA) - Company(Acme Corp) Integration operation: 1. HR system triggers employee creation 2. Application creates: Person(name: "John Doe") 3. Links: Person -[LIVES_IN]-> Country(USA) 4. Links: Person -[WORKS_AT]-> Company(Acme Corp) Final graph: Country(USA) <-[LIVES_IN]- Person(John) -[WORKS_AT]-> Company(Acme) Key difference from user-based creation: - Uses $_appId filter (auto-populated) - No user token required - Application is the authorized subject ## Requirements Prerequisites: - ServiceAccount credentials: For creating policies and queries (Bearer token) - AppAgent credentials: For data ingestion and query execution (X-IK-ClientKey) No user token needed: - Application acts as its own subject - $_appId automatically identifies the calling application ## Steps Step 1: Ingest Reference Data - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: POST Country and Company nodes - Result: Reference nodes ready for linking Step 2: Create Node Creation Policy - Authentication: ServiceAccount credential (Bearer token) - Action: POST policy allowing: - CREATE Person nodes - CREATE LIVES_IN relationships (Person -> Country) - CREATE WORKS_AT relationships (Person -> Company) - Result: Policy ID returned Step 3: Create Node Creation Query - Authentication: ServiceAccount credential (Bearer token) - Action: POST query with parameters: - $personId, $personName (for Person node) - $countryId (to link Country) - $companyId (to link Company) - Result: Query ID returned Step 4: Execute Creation - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: POST to /contx-iq/v1/execute with: - personId: "john-doe-123" - personName: "John Doe" - countryId: "usa" - companyId: "acme-corp" - Result: Person created with both relationships Step 5: Cleanup - Action: DELETE query and policy configurations ## Code Examples ### Step 1 Capture the nodes needed for this use case. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "Norway", "type": "Country" }, { "external_id": "Forsenty", "type": "Company" } ] } ``` ### Step 2 Policy which designates the derived query can create a Person node, a relationship with a Country node, and a relationship with a Company node. **policy.json** ```json { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "_Application" }, "condition": { "cypher": "MATCH (subject:_Application) MATCH (country:Country) MATCH (company:Company)", "filter": [ { "app": "app1", "operator": "AND", "operands": [ { "attribute": "subject.external_id", "operator": "=", "value": "$_appId" }, { "attribute": "country.external_id", "operator": "=", "value": "$country_external_id" }, { "attribute": "company.external_id", "operator": "=", "value": "$company_external_id" } ] } ] }, "allowed_reads": { "nodes": [ "country", "country.*", "company", "company.*" ] }, "allowed_upserts": { "nodes": { "node_types": [ "Person" ] }, "relationships": { "relationship_types": [ { "type": "BELONGS_TO", "source_node_label": "Person", "target_node_label": "Country" }, { "type": "BELONGS_TO", "source_node_label": "Person", "target_node_label": "Company" } ] } } } ``` Request to create a CIQ Policy configuration using REST. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"meta\":{\"policy_version\":\"1.0-ciq\"},\"subject\":{\"type\":\"_Application\"},\"condition\":{\"cypher\":\"MATCH (subject:_Application) MATCH (country:Country) MATCH (company:Company)\",\"filter\":[{\"app\":\"app1\",\"operator\":\"AND\",\"operands\":[{\"attribute\":\"subject.external_id\",\"operator\":\"=\",\"value\":\"$_appId\"},{\"attribute\":\"country.external_id\",\"operator\":\"=\",\"value\":\"$country_external_id\"},{\"attribute\":\"company.external_id\",\"operator\":\"=\",\"value\":\"$company_external_id\"}]}]},\"allowed_reads\":{\"nodes\":[\"country\",\"country.*\",\"company\",\"company.*\"]},\"allowed_upserts\":{\"nodes\":{\"node_types\":[\"Person\"]},\"relationships\":{\"relationship_types\":[{\"type\":\"BELONGS_TO\",\"source_node_label\":\"Person\",\"target_node_label\":\"Country\"},{\"type\":\"BELONGS_TO\",\"source_node_label\":\"Person\",\"target_node_label\":\"Company\"}]}}}", "status": "ACTIVE", "tags": [] } ``` Request to read the CIQ Policy configuration using REST. **policy_request.json** ```json { "id": "your_policy_configuration_gid" } ``` ### Step 3 The CIQ Knowledge Query designates the node to create and the relationships to upsert. **knowledge_query.json** ```json { "nodes": [ "person", "person.external_id" ], "relationships": [], "upsert_nodes": [ { "name": "person", "type": "Person", "external_id": "$person_external_id", "properties": [ { "type": "VerificationMethod", "value": "$verification_method", "metadata": [ { "type": "Assurance_level", "value": "$assurance_level" }, { "type": "source", "value": "$source" } ] }, { "type": "Email", "value": "$email", "metadata": [ { "type": "Assurance_level", "value": "$assurance_level" }, { "type": "source", "value": "$source" } ] }, { "type": "PhoneNumber", "value": "$phone_number", "metadata": [ { "type": "assurance_level", "value": "$assurance_level" }, { "type": "source", "value": "$source" } ] }, { "type": "FirstName", "value": "$first_name", "metadata": [ { "type": "assurance_level", "value": "$assurance_level" }, { "type": "source", "value": "$source" } ] }, { "type": "LastName", "value": "$last_name", "metadata": [ { "type": "assurance_level", "value": "$assurance_level" }, { "type": "source", "value": "$source" } ] } ] } ], "upsert_relationships": [ { "name": "rel1", "source": "person", "target": "country", "type": "BELONGS_TO", "properties": [] }, { "name": "rel2", "source": "person", "target": "company", "type": "BELONGS_TO", "properties": [] } ] } ``` Request to create a CIQ Knowledge Query configuration using REST. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "description of knowledge query", "display_name": "knowledge query name", "name": "knowledge-query-name", "policy_id": "your_policy_gid", "query": "{\"nodes\":[\"person\",\"person.external_id\"],\"relationships\":[],\"upsert_nodes\":[{\"name\":\"person\",\"type\":\"Person\",\"external_id\":\"$person_external_id\",\"properties\":[{\"type\":\"VerificationMethod\",\"value\":\"$verification_method\",\"metadata\":[{\"type\":\"Assurance_level\",\"value\":\"$assurance_level\"},{\"type\":\"source\",\"value\":\"$source\"}]},{\"type\":\"Email\",\"value\":\"$email\",\"metadata\":[{\"type\":\"Assurance_level\",\"value\":\"$assurance_level\"},{\"type\":\"source\",\"value\":\"$source\"}]},{\"type\":\"PhoneNumber\",\"value\":\"$phone_number\",\"metadata\":[{\"type\":\"assurance_level\",\"value\":\"$assurance_level\"},{\"type\":\"source\",\"value\":\"$source\"}]},{\"type\":\"FirstName\",\"value\":\"$first_name\",\"metadata\":[{\"type\":\"assurance_level\",\"value\":\"$assurance_level\"},{\"type\":\"source\",\"value\":\"$source\"}]},{\"type\":\"LastName\",\"value\":\"$last_name\",\"metadata\":[{\"type\":\"assurance_level\",\"value\":\"$assurance_level\"},{\"type\":\"source\",\"value\":\"$source\"}]}]}],\"upsert_relationships\":[{\"name\":\"rel1\",\"source\":\"person\",\"target\":\"country\",\"type\":\"BELONGS_TO\",\"properties\":[]},{\"name\":\"rel2\",\"source\":\"person\",\"target\":\"company\",\"type\":\"BELONGS_TO\",\"properties\":[]}]}", "status": "ACTIVE" } ``` Read the CIQ Query Configuration. **GET https://eu.api.indykite.com/configs/v1/knowledge-queries/{id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 4 CIQ Execution request in json format. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "knowledge_query_gid", "input_params": { "person_external_id": "456985", "verification_method": "BREGG", "assurance_level": 3, "source": "NAV", "email": "elias@email.com", "phone_number": "4725639685", "first_name": "Elias", "last_name": "Boomy", "country_external_id": "Norway", "company_external_id": "Forsenty" }, "page_token": 1 } ``` CIQ Execution response in json format. **response.json** ```json { "data": [ { "nodes": { "person": { "Id": 5, "ElementId": "4:a5c213aa-aa4b-4be5-a17a-a677a80ee634:5", "Labels": [ "Unique", "Resource", "Person" ], "Props": { "create_time": "2025-09-26T11:29:25.43Z", "external_id": "456985", "id": "I2kGlVoQQHmj_gfNvH4b-A", "type": "Person", "update_time": "2025-09-26T11:29:25.43Z" } }, "person.external_id": "456985" } } ] } ``` ### Step 5 Delete the CIQ Query. **DELETE https://eu.api.indykite.com/configs/v1/knowledge-queries/{id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` Delete the CIQ Policy. **DELETE https://eu.api.indykite.com/configs/v1/authorization-policies/{id}** ```json { "id": "your_policy_configuration_gid" } ``` ## Common Errors ### 401: UNAUTHENTICATED **Solution:** Check credentials: ServiceAccount Bearer token for config APIs, X-IK-ClientKey for execution ### 404: NOT_FOUND **Solution:** Verify the query/policy ID exists and belongs to your project --- Source: https://developer.indykite.com/resources/ciq-17 --- # ContX IQ: Wildcard Property Retrieval - Fetch All Properties > Use wildcard syntax to retrieve all properties of a node or relationship without explicitly listing each one. Simplifies queries when you need complete data snapshots or don't know all property names in advance. **Category:** ContX IQ **API:** ContX IQ **Tags:** ContX IQ Policy, ContX IQ Query, Wildcard Properties, Dynamic Schema, All Properties, Flexible Queries **Last Updated:** 2026-03-20 **OpenAPI Endpoints:** /capture/v1/nodes, /capture/v1/relationships, /configs/v1/authorization-policies, /configs/v1/knowledge-queries, /contx-iq/v1/execute **Related Guides:** /guides/guide-contx-iq, /guides/guide-sandbox ## Summary This example demonstrates wildcard property retrieval: Traditional approach (explicit): RETURN car.model, car.vin, car.color, car.year Wildcard approach: RETURN car.* (returns ALL properties) Mixed approach in this example: - LicenseNumber: Retrieve specific property (number) - Car: Retrieve ALL properties (using wildcard) - OWNS relationship: Retrieve ALL properties (using wildcard) Benefits: - Handles dynamic/evolving schemas - Simpler queries for complete data exports - No need to update queries when properties are added ## Use Case Scenario: Export complete vehicle data without knowing all property names. Graph data: Person(Alice) -[OWNS {weight: "100kg", since: "2020"}]-> Car(model: "Mustang", vin: "ABC", color: "red", year: 2020) Car -[HAS]-> LicenseNumber(number: "ABC-123") Query returns: { license: "ABC-123", // Specific property car: {model, vin, color, year, ...any_future_properties}, // All Car properties owns: {weight, since, ...any_future_properties} // All OWNS properties } Future-proof: If Car gains new properties (mileage, insurance), query automatically includes them. ## Requirements Prerequisites: - ServiceAccount credentials: For creating policies and queries (Bearer token) - AppAgent credentials: For data ingestion and query execution (X-IK-ClientKey) - User access token: For authorized read operations Policy considerations: - Wildcard read access must be authorized in policy - Policy can still restrict which node types allow wildcard reads ## Steps Step 1: Ingest Nodes with Multiple Properties - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: POST nodes with various properties - Result: Graph with rich property data Step 2: Create Wildcard Read Policy - Authentication: ServiceAccount credential (Bearer token) - Action: POST policy allowing: - READ specific properties on some nodes - READ all properties (*) on others - Result: Policy ID returned Step 3: Create Wildcard Query - Authentication: ServiceAccount credential (Bearer token) - Action: POST query using: - Specific: licenseNumber.number - Wildcard: car.*, owns.* - Result: Query ID returned Step 4: Execute Query - Authentication: AppAgent credential + User token - Action: POST to /contx-iq/v1/execute - Result: Complete property data for Car and OWNS, specific property for LicenseNumber ## Code Examples ### Step 1 Capture the nodes needed for this use case. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "alice", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "alice@email.com" }, { "type": "name", "value": "Alice Smith" } ] }, { "external_id": "satchmo", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "satchmo@demo.com" }, { "type": "name", "value": "Louis Armstrong" } ] }, { "external_id": "karel", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "karel@demo.com" }, { "type": "name", "value": "Karel Plihal" } ] }, { "external_id": "kitt", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "pontiac" }, { "type": "model", "value": "Firebird" } ] }, { "external_id": "caddilacv16", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "Caddilac" }, { "type": "model", "value": "V-16" } ] }, { "external_id": "harmonika", "type": "Bus", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "Ikarus" }, { "type": "model", "value": "280" } ] }, { "external_id": "ln-xxx", "type": "LicenseNumber", "is_identity": false, "properties": [ { "type": "license", "value": "ln-xxx-value" } ] }, { "external_id": "ln-yyy", "type": "LicenseNumber", "is_identity": false, "properties": [ { "type": "license", "value": "ln-yyy-value" } ] }, { "external_id": "ln-zzz", "type": "LicenseNumber", "is_identity": false, "properties": [ { "type": "license", "value": "ln-zzz-value" } ] } ] } ``` Capture the relationships needed for this use case. **POST https://eu.api.indykite.com/capture/v1/relationships/** ```json { "relationships": [ { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "kitt", "type": "Car" }, "type": "OWNS", "properties": [ { "type": "weight", "value": 1 } ] }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "caddilacv16", "type": "Car" }, "type": "OWNS", "properties": [ { "type": "weight", "value": 2 } ] }, { "source": { "external_id": "satchmo", "type": "Person" }, "target": { "external_id": "caddilacv16", "type": "Car" }, "type": "OWNS" }, { "source": { "external_id": "karel", "type": "Person" }, "target": { "external_id": "harmonika", "type": "Bus" }, "type": "OWNS" }, { "source": { "external_id": "kitt", "type": "Car" }, "target": { "external_id": "ln-xxx", "type": "LicenseNumber" }, "type": "HAS", "properties": [ { "type": "weight", "value": 3 } ] }, { "source": { "external_id": "caddilacv16", "type": "Car" }, "target": { "external_id": "ln-zzz", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "harmonika", "type": "Bus" }, "target": { "external_id": "ln-yyy", "type": "LicenseNumber" }, "type": "HAS" } ] } ``` ### Step 2 Create a CIQ Policy which designates the Subject node, the cypher, the nodes allowed to be upserted and the nodes allowed to be read. **policy.json** ```json { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "Person" }, "condition": { "cypher": "MATCH (subject:Person)-[owns:OWNS]->(car:Car)-[:HAS]->(ln:LicenseNumber)", "filter": [ { "app": "app1", "operator": "AND", "operands": [ { "operator": "=", "attribute": "subject.external_id", "value": "$subject_external_id" }, { "attribute": "$token.sub", "operator": "=", "value": "$token_sub" } ] } ] }, "allowed_reads": { "nodes": [ "car", "ln", "car.*", "ln.property.license" ], "relationships": [ "owns.*" ] }, "allowed_upserts": { "nodes": { "existing_nodes": [ "car" ] } } } ``` Request to create the CIQ Policy configuration using REST. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"meta\":{\"policy_version\":\"1.0-ciq\"},\"subject\":{\"type\":\"Person\"},\"condition\":{\"cypher\":\"MATCH (subject:Person)-[owns:OWNS]->(car:Car)-[:HAS]->(ln:LicenseNumber)\",\"filter\":[{\"app\":\"app1\",\"operator\":\"AND\",\"operands\":[{\"operator\":\"=\",\"attribute\":\"subject.external_id\",\"value\":\"$subject_external_id\"},{\"attribute\":\"$token.sub\",\"operator\":\"=\",\"value\":\"$token_sub\"}]}]},\"allowed_reads\":{\"nodes\":[\"car\",\"ln\",\"car.*\",\"ln.property.license\"],\"relationships\":[\"owns.*\"]},\"allowed_upserts\":{\"nodes\":{\"existing_nodes\":[\"car\"]}}}", "status": "ACTIVE", "tags": [] } ``` Request to read the CIQ Policy configuration using REST. **policy_request.json** ```json { "id": "your_policy_configuration_gid" } ``` ### Step 3 Create a CIQ Query in the context of the policy to retrieve the designated properties. **knowledge_query.json** ```json { "nodes": [ "car.property.*", "ln.property.license" ], "relationships": [ "owns.*" ], "filter": { "attribute": "ln.property.license", "operator": "=", "value": "$ln_value" } } ``` Request to create a CIQ Query configuration using REST. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "description of knowledge query", "display_name": "knowledge query name", "name": "knowledge-query-name", "policy_id": "your_policy_gid", "query": "{\"nodes\":[\"car.property.*\",\"ln.property.license\"],\"relationships\":[\"owns.*\"],\"filter\":{\"attribute\":\"ln.property.license\",\"operator\":\"=\",\"value\":\"$ln_value\"}}", "status": "ACTIVE" } ``` Read the CIQ Query Configuration. **GET https://eu.api.indykite.com/configs/v1/knowledge-queries/{id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 4 Run a CIQ Execution to get the designated information. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "knowledge_query_gid", "input_params": { "ln_value": "ln-xxx-value", "subject_external_id": "alice", "token_sub": "alice_user_external_id" }, "page_token": 1 } ``` CIQ Execution response. **response.json** ```json { "data": [ { "nodes": { "car.property.*": [ { "type": "manufacturer", "value": "pontiac" }, { "type": "model", "value": "Firebird" } ], "ln.property.license": "ln-xxx-value" }, "relationships": { "owns.*": { "create_time": "2025-09-26T20:41:30.318Z", "id": "Wb01b9HYQgKbGkpQl4FDeA", "type": "OWNS", "update_time": "2025-09-26T20:41:30.318Z", "weight": 1 } } } ] } ``` ## Common Errors ### 401: UNAUTHENTICATED **Solution:** Check credentials: ServiceAccount Bearer token for config APIs, X-IK-ClientKey for execution ### 404: NOT_FOUND **Solution:** Verify the query/policy ID exists and belongs to your project --- Source: https://developer.indykite.com/resources/ciq-18 --- # ContX IQ: Retrieve All Directly Connected Nodes for a User > Query all nodes that have a direct relationship with a specified User node, regardless of relationship type or direction. Useful for user profile views, data export, or understanding a user's complete data footprint. **Category:** ContX IQ **API:** ContX IQ **Tags:** ContX IQ Policy, ContX IQ Query, Graph Traversal, Related Nodes, User Data, Direct Relationships **Last Updated:** 2026-03-20 **OpenAPI Endpoints:** /capture/v1/nodes, /capture/v1/relationships, /configs/v1/authorization-policies, /configs/v1/knowledge-queries, /contx-iq/v1/execute **Related Guides:** /guides/guide-contx-iq, /guides/guide-sandbox ## Summary This example retrieves all directly connected nodes for a user: Query pattern: MATCH (user:User {id: $userId})-[r]-(connected) RETURN connected Returns all nodes with ANY direct relationship to the user: - Incoming relationships: (other)-[r]->(user) - Outgoing relationships: (user)-[r]->(other) - Any relationship type Use cases: - User profile aggregation - GDPR data export (all user-related data) - Impact analysis (what's connected to this user?) ## Use Case Scenario: Build a user dashboard showing all related entities. User graph: Person(Alice) -[OWNS]-> Car(Mustang) -[HAS]-> Email(alice@example.com) -[WORKS_AT]-> Company(Acme) -[LIVES_IN]-> Country(USA) <-[MANAGES]- Manager(Bob) Query input: userId = "alice" Query result: [ {type: "Car", id: "mustang", relationship: "OWNS"}, {type: "Email", id: "alice@example.com", relationship: "HAS"}, {type: "Company", id: "acme", relationship: "WORKS_AT"}, {type: "Country", id: "usa", relationship: "LIVES_IN"}, {type: "Person", id: "bob", relationship: "MANAGES"} ] All direct connections returned regardless of direction or type. ## Requirements Prerequisites: - ServiceAccount credentials: For creating policies and queries (Bearer token) - AppAgent credentials: For data ingestion and query execution (X-IK-ClientKey) Application as subject: - Uses $_appId filter (auto-populated) - No user token required for this pattern ## Steps Step 1: Ingest Users and Related Nodes - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: POST User nodes and various connected nodes with different relationship types - Result: Rich user graph ready for traversal Step 2: Create Related Nodes Policy - Authentication: ServiceAccount credential (Bearer token) - Action: POST policy allowing READ on nodes connected to users - Result: Policy ID returned Step 3: Create Related Nodes Query - Authentication: ServiceAccount credential (Bearer token) - Action: POST query that: - Matches User by input parameter - Traverses all relationships (any type, any direction) - Returns connected nodes with relationship info - Result: Query ID returned Step 4: Execute Query - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: POST to /contx-iq/v1/execute with userId parameter - Result: Array of all directly connected nodes 5. Delete your configuration. ## Code Examples ### Step 1 Capture the nodes needed for this use case. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "nico", "type": "User", "is_identity": true, "properties": [ { "type": "email", "value": "nico@email.com" } ] }, { "external_id": "fred", "type": "User", "is_identity": true, "properties": [ { "type": "email", "value": "fred@email.com" } ] }, { "external_id": "customer", "type": "Role", "properties": [ { "type": "source", "value": "Head" } ] }, { "external_id": "norway", "type": "Country", "properties": [ { "type": "name", "value": "Norway" } ] }, { "external_id": "sweden", "type": "Country", "properties": [ { "type": "name", "value": "Sweden" } ] }, { "external_id": "123456", "type": "Account", "properties": [ { "type": "location", "value": "CityHub" } ] }, { "external_id": "234567", "type": "Account", "properties": [ { "type": "location", "value": "Palace" } ] }, { "external_id": "tyrest", "type": "Company", "properties": [ { "type": "name", "value": "Tyrest" } ] }, { "external_id": "blue", "type": "Company", "properties": [ { "type": "name", "value": "Blue" } ] } ] } ``` Capture the relationships needed for this use case. **POST https://eu.api.indykite.com/capture/v1/relationships/** ```json { "relationships": [ { "source": { "external_id": "nico", "type": "User" }, "target": { "external_id": "tyrest", "type": "Company" }, "type": "PART_OF", "properties": [ { "type": "weight", "value": 1 } ] }, { "source": { "external_id": "nico", "type": "User" }, "target": { "external_id": "norway", "type": "Country" }, "type": "WORKS_IN" }, { "source": { "external_id": "customer", "type": "Role" }, "target": { "external_id": "nico", "type": "User" }, "type": "ASSIGNED_TO" }, { "source": { "external_id": "123456", "type": "Account" }, "target": { "external_id": "nico", "type": "User" }, "type": "BELONGS_TO" }, { "source": { "external_id": "fred", "type": "User" }, "target": { "external_id": "sweden", "type": "Country" }, "type": "WORKS_IN" }, { "source": { "external_id": "customer", "type": "Role" }, "target": { "external_id": "fred", "type": "User" }, "type": "ASSIGNED_TO" }, { "source": { "external_id": "234567", "type": "Account" }, "target": { "external_id": "fred", "type": "User" }, "type": "BELONGS_TO" } ] } ``` ### Step 2 Policy which designates the derived query can retrieve all nodes directly related to a User node. **policy.json** ```json { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "_Application" }, "condition": { "cypher": "MATCH (subject:_Application) MATCH (u:User)-[]-(n:Resource)", "filter": [ { "app": "app1", "operator": "AND", "operands": [ { "attribute": "subject.external_id", "operator": "=", "value": "$_appId" }, { "attribute": "u.external_id", "operator": "=", "value": "$user_external_id" } ] } ] }, "allowed_reads": { "nodes": [ "u", "u.*", "n", "n.*" ] } } ``` Request to create a CIQ Policy configuration using REST. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"meta\":{\"policy_version\":\"1.0-ciq\"},\"subject\":{\"type\":\"_Application\"},\"condition\":{\"cypher\":\"MATCH (subject:_Application) MATCH (u:User)-[]-(n:Resource)\",\"filter\":[{\"app\":\"app1\",\"operator\":\"AND\",\"operands\":[{\"attribute\":\"subject.external_id\",\"operator\":\"=\",\"value\":\"$_appId\"},{\"attribute\":\"u.external_id\",\"operator\":\"=\",\"value\":\"$user_external_id\"}]}]},\"allowed_reads\":{\"nodes\":[\"u\",\"u.*\",\"n\",\"n.*\"]}}", "status": "ACTIVE", "tags": [] } ``` Request to read the CIQ Policy configuration using REST. **policy_request.json** ```json { "id": "your_policy_configuration_gid" } ``` ### Step 3 The CIQ Knowledge Query designates the information you want to retrieve. **knowledge_query.json** ```json { "nodes": [ "n.external_id", "n.type", "n.property.*" ] } ``` Request to create a CIQ Knowledge Query configuration using REST. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "description of knowledge query", "display_name": "knowledge query name", "name": "knowledge-query-name", "policy_id": "your_policy_gid", "query": "{\"nodes\":[\"n.external_id\",\"n.type\",\"n.property.*\"]}", "status": "ACTIVE" } ``` Read the CIQ Query Configuration. **GET https://eu.api.indykite.com/configs/v1/knowledge-queries/{id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 4 CIQ Execution request in json format. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "knowledge_query_gid", "input_params": { "user_external_id": "nico" }, "page_token": 1 } ``` CIQ Execution response in json format. **response.json** ```json { "data": [ { "nodes": { "n.external_id": "123456", "n.property.*": [ { "type": "location", "value": "CityHub" } ], "n.type": "Account" } }, { "nodes": { "n.external_id": "customer", "n.property.*": [ { "type": "source", "value": "Head" } ], "n.type": "Role" } }, { "nodes": { "n.external_id": "norway", "n.property.*": [ { "type": "name", "value": "Norway" } ], "n.type": "Country" } }, { "nodes": { "n.external_id": "tyrest", "n.property.*": [ { "type": "name", "value": "Tyrest" } ], "n.type": "Company" } } ] } ``` ### Step 5 Delete the CIQ Query. **DELETE https://eu.api.indykite.com/configs/v1/knowledge-queries/{id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` Delete the CIQ Policy. **DELETE https://eu.api.indykite.com/configs/v1/authorization-policies/{id}** ```json { "id": "your_policy_configuration_gid" } ``` ## Common Errors ### 401: UNAUTHENTICATED **Solution:** Check credentials: ServiceAccount Bearer token for config APIs, X-IK-ClientKey for execution ### 404: NOT_FOUND **Solution:** Verify the query/policy ID exists and belongs to your project --- Source: https://developer.indykite.com/resources/ciq-19 --- # ContX IQ: Retrieve Payment Methods for Contracted Vehicle Users > Query the IndyKite Knowledge Graph to list all payment methods belonging to people who have active contracts for vehicles. This demonstrates linking an Application to graph data and querying through authorized relationships. **Category:** ContX IQ **API:** ContX IQ **Tags:** ContX IQ Policy, ContX IQ Query, ContX IQ Execution, Write Query, Application Linking, Payment Data **Last Updated:** 2026-03-20 **OpenAPI Endpoints:** /capture/v1/nodes, /capture/v1/relationships, /configs/v1/authorization-policies, /configs/v1/knowledge-queries, /contx-iq/v1/execute **Related Guides:** /guides/guide-contx-iq, /guides/guide-sandbox ## Summary This example demonstrates a two-phase ContX IQ workflow: Phase 1 - Link Application to Graph Data: 1. Create a policy allowing the Application to create relationships 2. Create a write query that links _Application to Company nodes 3. Execute the query to create HAS_AGREEMENT_WITH relationship Phase 2 - Query Payment Methods: 4. Create a policy allowing read access to PaymentMethod nodes through the relationship path 5. Create a query to retrieve payment methods 6. Execute and get results Key concept: The _Application node (auto-created when credentials are generated) must be linked to business data before it can query through those relationships. ## Use Case Scenario: Your application has a business agreement with Company1, which owns a fleet of vehicles. Goal: Retrieve all payment methods for people who have contracts on Company1's vehicles. Graph traversal path: _Application -[HAS_AGREEMENT_WITH]-> Company1 -[OWNS]-> Vehicle -[COVERS]<- Contract -[ACCEPTED]<- Person -[HAS]-> PaymentMethod Expected result: Payment method details for all people with active vehicle contracts under Company1. ## Requirements Prerequisites: - ServiceAccount credentials: For creating policies and queries (Bearer token authentication) - AppAgent credentials: For data ingestion and query execution (X-IK-ClientKey header) Required API access: - POST /capture/v1/nodes/ and /capture/v1/relationships/ (data ingestion) - POST /configs/v1/authorization-policies (create policies) - POST /configs/v1/knowledge-queries (create queries) - POST /contx-iq/v1/execute (run queries) ## Steps Step 1: Ingest Graph Data - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: POST nodes and relationships to create the graph structure - Result: Person, Company, Vehicle, Contract, PaymentMethod nodes with relationships Step 2: Create Application Linking Policy - Authentication: ServiceAccount credential (Bearer token) - Action: POST policy that allows the _Application node to create HAS_AGREEMENT_WITH relationships - Key detail: Uses $_appId filter which auto-resolves to the calling Application's ID - Result: Policy ID for the linking operation Step 3: Create Application Linking Query - Authentication: ServiceAccount credential (Bearer token) - Action: POST a write query that creates _Application -[HAS_AGREEMENT_WITH]-> Company relationship - Result: Query ID for execution Step 4: Execute Linking Query - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: POST to /contx-iq/v1/execute with the linking query ID - Result: _Application node is now connected to Company1 in the graph Step 5: Create Payment Read Policy - Authentication: ServiceAccount credential (Bearer token) - Action: POST policy that allows reading PaymentMethod nodes through the established relationship path - Result: Policy ID for payment queries Step 6: Create Payment Query - Authentication: ServiceAccount credential (Bearer token) - Action: POST query that traverses from Application through Company, Vehicle, Contract, Person to PaymentMethod - Result: Query ID for execution Step 7: Execute Payment Query - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: POST to /contx-iq/v1/execute with the payment query ID - Result: Array of payment method data for authorized users Step 8: Cleanup - Action: DELETE queries and policies (does not affect graph data) ## Code Examples ### Step 1a Capture nodes into the IKG. Creates Person, Company, Vehicle, Contract, LicenseNumber, and PaymentMethod nodes. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "alice", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "alice@email.com" }, { "type": "given_name", "value": "Alice" }, { "type": "last_name", "value": "Smith" } ] }, { "external_id": "ryan", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "ryan@yahoo.co.uk" }, { "type": "given_name", "value": "ryan" }, { "type": "last_name", "value": "mushu" } ] }, { "external_id": "tilda", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "tilda@yahoo.co.uk" }, { "type": "given_name", "value": "tilda" }, { "type": "last_name", "value": "mushu" } ] }, { "external_id": "cb123", "type": "PaymentMethod", "properties": [ { "type": "payment_name", "value": "Credit Card" } ] }, { "external_id": "kl123", "type": "PaymentMethod", "properties": [ { "type": "payment_name", "value": "Klarna" } ] }, { "external_id": "ct123", "type": "Contract", "properties": [ { "type": "category", "value": "Insurance" }, { "type": "status", "value": "Active" }, { "type": "number", "value": "hfgrten123", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "ct234", "type": "Contract", "properties": [ { "type": "category", "value": "Insurance" }, { "type": "status", "value": "Active" }, { "type": "number", "value": "hfgrten234", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "ct985", "type": "Contract", "properties": [ { "type": "category", "value": "Insurance" }, { "type": "status", "value": "Active" }, { "type": "number", "value": "hfgrten985", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "car1", "type": "Vehicle", "properties": [ { "type": "category", "value": "Car" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "rtfhcnvjt471" } ] }, { "external_id": "car2", "type": "Vehicle", "properties": [ { "type": "category", "value": "Car" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "kdcbfrt178" } ] }, { "external_id": "truck1", "type": "Vehicle", "properties": [ { "type": "category", "value": "Truck" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "sncnrkcldp" } ] }, { "external_id": "license1", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "AX123456", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "license2", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "OL123456", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "license3", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "VN123456", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "company1", "type": "Company", "properties": [ { "type": "name", "value": "Company1" }, { "type": "registration", "value": "256314523" } ] }, { "external_id": "company2", "type": "Company", "properties": [ { "type": "name", "value": "Company2" }, { "type": "registration", "value": "942365123" } ] }, { "external_id": "application1", "type": "Application", "properties": [ { "type": "name", "value": "Application" } ] }, { "external_id": "application2", "type": "Application", "properties": [ { "type": "name", "value": "Application2" } ] } ] } ``` ### Step 1b Capture relationships between nodes. Establishes OWNS, ACCEPTED, COVERS, HAS connections. **POST https://eu.api.indykite.com/capture/v1/relationships/** ```json { "relationships": [ { "source": { "external_id": "ryan", "type": "Person" }, "target": { "external_id": "cb123", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "tilda", "type": "Person" }, "target": { "external_id": "kl123", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "cb123", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "ryan", "type": "Person" }, "target": { "external_id": "ct123", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "tilda", "type": "Person" }, "target": { "external_id": "ct234", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "ct985", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "ct123", "type": "Contract" }, "target": { "external_id": "car1", "type": "Vehicle" }, "type": "COVERS" }, { "source": { "external_id": "ct985", "type": "Contract" }, "target": { "external_id": "car1", "type": "Vehicle" }, "type": "COVERS" }, { "source": { "external_id": "ct234", "type": "Contract" }, "target": { "external_id": "truck1", "type": "Vehicle" }, "type": "COVERS" }, { "source": { "external_id": "car1", "type": "Vehicle" }, "target": { "external_id": "license1", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "truck1", "type": "Vehicle" }, "target": { "external_id": "license2", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "car2", "type": "Vehicle" }, "target": { "external_id": "license3", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "company1", "type": "Company" }, "target": { "external_id": "car1", "type": "Vehicle" }, "type": "OWNS" }, { "source": { "external_id": "company1", "type": "Company" }, "target": { "external_id": "car2", "type": "Vehicle" }, "type": "OWNS" }, { "source": { "external_id": "company1", "type": "Company" }, "target": { "external_id": "truck1", "type": "Vehicle" }, "type": "OWNS" }, { "source": { "external_id": "application1", "type": "Application" }, "target": { "external_id": "company1", "type": "Company" }, "type": "HAS_AGREEMENT_WITH" }, { "source": { "external_id": "application2", "type": "Application" }, "target": { "external_id": "company1", "type": "Company" }, "type": "HAS_AGREEMENT_WITH" } ] } ``` ### Step 2a Policy JSON allowing _Application to create HAS_AGREEMENT_WITH relationships. The $_appId filter auto-matches the calling Application. **policy.json** ```json { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "_Application" }, "condition": { "cypher": "MATCH (subject:_Application) MATCH (company:Company)-[r2:OWNS]->(vehicle:Vehicle)", "filter": [ { "operator": "AND", "operands": [ { "attribute": "subject.external_id", "operator": "=", "value": "$_appId" }, { "attribute": "company.external_id", "operator": "=", "value": "$companyID" } ] } ] }, "allowed_upserts": { "relationships": { "relationship_types": [ { "type": "HAS_AGREEMENT_WITH", "source_node_label": "_Application", "target_node_label": "Company" } ] } }, "allowed_reads": { "nodes": [ "company.*", "subject.*" ] } } ``` ### Step 2b POST request to create the application linking policy. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"meta\":{\"policy_version\":\"1.0-ciq\"},\"subject\":{\"type\":\"_Application\"},\"condition\":{\"cypher\":\"MATCH (subject:_Application) MATCH (company:Company)-[r2:OWNS]->(vehicle:Vehicle)\",\"filter\":[{\"operator\":\"AND\",\"operands\":[{\"attribute\":\"subject.external_id\",\"operator\":\"=\",\"value\":\"$_appId\"},{\"attribute\":\"company.external_id\",\"operator\":\"=\",\"value\":\"$companyID\"}]}]},\"allowed_upserts\":{\"relationships\":{\"relationship_types\":[{\"type\":\"HAS_AGREEMENT_WITH\",\"source_node_label\":\"_Application\",\"target_node_label\":\"Company\"}]}},\"allowed_reads\":{\"nodes\":[\"company.*\",\"subject.*\"]}}", "status": "ACTIVE", "tags": [] } ``` ### Step 2c GET request to verify the policy was created successfully. **GET https://eu.api.indykite.com/configs/v1/authorization-policies/{policy_id}** ```json { "id": "your_policy_configuration_gid" } ``` ### Step 3a Write query JSON that creates the HAS_AGREEMENT_WITH relationship between _Application and a Company node. **knowledge_query.json** ```json { "nodes": [ "subject.external_id" ], "relationships": [ "r1" ], "upsert_relationships": [ { "name": "r1", "source": "subject", "target": "company", "type": "HAS_AGREEMENT_WITH" } ] } ``` ### Step 3b POST request to create the application linking query. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "description of knowledge query", "display_name": "knowledge query name", "name": "knowledge-query-name", "policy_id": "your_policy_gid", "query": "{\"nodes\":[\"subject.external_id\"],\"relationships\":[\"r1\"],\"upsert_relationships\":[{\"name\":\"r1\",\"source\":\"subject\",\"target\":\"company\",\"type\":\"HAS_AGREEMENT_WITH\"}]}", "status": "ACTIVE" } ``` ### Step 3c GET request to verify the query was created. **GET https://eu.api.indykite.com/configs/v1/knowledge-queries/{query_id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 4a Execute the linking query. Creates _Application -[HAS_AGREEMENT_WITH]-> Company relationship in the graph. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "ciq_query_gid", "input_params": { "companyID": "company1" } } ``` ### Step 4b Response confirming the relationship was created. **response.json** ```json { "data": [ { "nodes": { "subject.external_id": "application_external_id" }, "relationships": { "r1": { "Id": 1152932499723124700, "ElementId": "5:3a2b09d5-2923-45d7-8453-8b1c698427b0:1152932499723124736", "StartId": 0, "StartElementId": "4:3a2b09d5-2923-45d7-8453-8b1c698427b0:0", "EndId": 15, "EndElementId": "4:3a2b09d5-2923-45d7-8453-8b1c698427b0:15", "Type": "HAS_AGREEMENT_WITH", "Props": { "create_time": "2025-06-09T15:12:46.374Z", "id": "48BJHS2CTFKcVpD4cUF8IA", "update_time": "2025-06-09T15:12:46.374Z" } } } }, { "nodes": { "subject.external_id": "application_external_id" }, "relationships": { "r1": { "Id": 1152932499723124700, "ElementId": "5:3a2b09d5-2923-45d7-8453-8b1c698427b0:1152932499723124736", "StartId": 0, "StartElementId": "4:3a2b09d5-2923-45d7-8453-8b1c698427b0:0", "EndId": 15, "EndElementId": "4:3a2b09d5-2923-45d7-8453-8b1c698427b0:15", "Type": "HAS_AGREEMENT_WITH", "Props": { "create_time": "2025-06-09T15:12:46.374Z", "id": "48BJHS2CTFKcVpD4cUF8IA", "update_time": "2025-06-09T15:12:46.374Z" } } } }, { "nodes": { "subject.external_id": "application_external_id" }, "relationships": { "r1": { "Id": 1152932499723124700, "ElementId": "5:3a2b09d5-2923-45d7-8453-8b1c698427b0:1152932499723124736", "StartId": 0, "StartElementId": "4:3a2b09d5-2923-45d7-8453-8b1c698427b0:0", "EndId": 15, "EndElementId": "4:3a2b09d5-2923-45d7-8453-8b1c698427b0:15", "Type": "HAS_AGREEMENT_WITH", "Props": { "create_time": "2025-06-09T15:12:46.374Z", "id": "48BJHS2CTFKcVpD4cUF8IA", "update_time": "2025-06-09T15:12:46.374Z" } } } } ] } ``` ### Step 5a Policy JSON allowing _Application to READ PaymentMethod nodes through the relationship path: Application -> Company -> Vehicle -> Contract -> Person -> PaymentMethod. **policy.json** ```json { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "_Application" }, "condition": { "cypher": "MATCH (subject:_Application)-[r1:HAS_AGREEMENT_WITH]->(company:Company)-[r2:OWNS]->(vehicle:Vehicle)-[r3:HAS]->(ln:LicenseNumber) MATCH (vehicle)<-[r4:COVERS]-(contract:Contract)<-[r5:ACCEPTED]-(person:Person)-[r6:HAS]->(pm:PaymentMethod)", "filter": [ { "attribute": "subject.external_id", "operator": "=", "value": "$_appId" } ] }, "allowed_reads": { "nodes": [ "pm.property.*", "person.property.*", "vehicle.property.is_active" ], "relationships": [] } } ``` ### Step 5b POST request to create the payment read policy. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"meta\":{\"policy_version\":\"1.0-ciq\"},\"subject\":{\"type\":\"_Application\"},\"condition\":{\"cypher\":\"MATCH (subject:_Application)-[r1:HAS_AGREEMENT_WITH]->(company:Company)-[r2:OWNS]->(vehicle:Vehicle)-[r3:HAS]->(ln:LicenseNumber) MATCH (vehicle)<-[r4:COVERS]-(contract:Contract)<-[r5:ACCEPTED]-(person:Person)-[r6:HAS]->(pm:PaymentMethod)\",\"filter\":[{\"attribute\":\"subject.external_id\",\"operator\":\"=\",\"value\":\"$_appId\"}]},\"allowed_reads\":{\"nodes\":[\"pm.property.*\",\"person.property.*\",\"vehicle.property.is_active\"],\"relationships\":[]}}", "status": "ACTIVE", "tags": [] } ``` ### Step 5b (Python) Python SDK equivalent: Creates the payment read policy. **create_policy.py** ```python import http.client conn = http.client.HTTPSConnection("eu.api.indykite.com") payload = "{"description": "", "display_name": "", "name": "", "policy": "", "project_id": "", "status": "ACTIVE", "tags": [ "" ]}" headers = { 'Content-Type': "application/json", 'Authorization': "YOUR_SECRET_TOKEN" } conn.request("POST", "/configs/v1/authorization-policies", payload, headers) res = conn.getresponse() data = res.read() ``` ### Step 5c GET request to verify the policy was created. **GET https://eu.api.indykite.com/configs/v1/authorization-policies/{policy_id}** ```json { "id": "your_policy_configuration_gid" } ``` ### Step 5c (Python) Python SDK equivalent: Reads the policy to verify creation. **read_policy.py** ```python import http.client conn = http.client.HTTPSConnection("eu.api.indykite.com") headers = { 'Authorization': "YOUR_SECRET_TOKEN" } conn.request("GET", "/configs/v1/authorization-policies/{{id}}", headers=headers) res = conn.getresponse() data = res.read() ``` ### Step 6a Query JSON that traverses the graph to retrieve PaymentMethod data for contracted users. **knowledge_query.json** ```json { "nodes": [ "vehicle.property.is_active", "person.property.email", "pm.property.payment_name" ] } ``` ### Step 6b POST request to create the payment query. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "description of knowledge query", "display_name": "knowledge query name", "name": "knowledge-query-name", "policy_id": "your_policy_gid", "query": "{\"nodes\":[\"vehicle.property.is_active\",\"person.property.email\",\"pm.property.payment_name\"]}", "status": "ACTIVE" } ``` ### Step 6b (Python) Python SDK equivalent: Creates the payment query. **create_knowledge_query.py** ```python import http.client conn = http.client.HTTPSConnection("eu.api.indykite.com") payload = "{"description": "", "display_name": "", "name": "", "policy_id": "", "project_id": "", "query": "", "status": "ACTIVE"}" headers = { 'Content-Type': "application/json", 'Authorization': "YOUR_SECRET_TOKEN" } conn.request("POST", "/configs/v1/knowledge-queries", payload, headers) res = conn.getresponse() data = res.read() ``` ### Step 6c GET request to verify the query was created. **GET https://eu.api.indykite.com/configs/v1/knowledge-queries/{query_id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 7a Execute the payment query to retrieve authorized payment method data. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "knowledge_query_gid", "input_params": {} } ``` ### Step 7b Response containing payment method data for people with active vehicle contracts. **response.json** ```json { "data": [ { "nodes": { "person.property.email": "alice@email.com", "pm.property.payment_name": "Credit Card", "vehicle.property.is_active": true } }, { "nodes": { "person.property.email": "ryan@yahoo.co.uk", "pm.property.payment_name": "Credit Card", "vehicle.property.is_active": true } }, { "nodes": { "person.property.email": "tilda@yahoo.co.uk", "pm.property.payment_name": "Klarna", "vehicle.property.is_active": true } } ] } ``` ### Step 7a (Python) Python SDK equivalent: Executes the payment query. **execute_query.py** ```python import http.client conn = http.client.HTTPSConnection("eu.api.indykite.com") payload = "{"id": "knowledge_query_gid", "input_params": {"ln_number": "AX123456","app_external_id": "application1"} }" headers = { 'Content-Type': "application/json", 'Authorization': "YOUR_SECRET_TOKEN" } conn.request("POST", "/contx-iq/v1/execute", payload, headers) res = conn.getresponse() data = res.read() ``` ### Step 8a DELETE request to remove the knowledge queries. **DELETE https://eu.api.indykite.com/configs/v1/knowledge-queries/{query_id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 8a (Python) Python SDK equivalent: Deletes the queries. **delete_queries.py** ```python import http.client conn = http.client.HTTPSConnection("eu.api.indykite.com) headers = { 'Authorization': "Bearer ..." } conn.request("DELETE", "/configs/v1/knowledge-queries/{id}", headers=headers) res = conn.getresponse() data = res.read() print(data.decode("utf-8")) ``` ### Step 8b DELETE request to remove the authorization policies. **DELETE https://eu.api.indykite.com/configs/v1/authorization-policies/{policy_id}** ```json { "id": "your_policy_configuration_gid" } ``` ### Step 8b (Python) Python SDK equivalent: Deletes the policies. **delete_policies.py** ```python import http.client conn = http.client.HTTPSConnection("eu.api.indykite.com") headers = { 'Authorization': "Bearer ...", 'Content-Type': "application/json" } conn.request("DELETE", "/configs/v1/authorization-policies/{id}", headers=headers) res = conn.getresponse() data = res.read() ``` ## Common Errors ### 401: UNAUTHENTICATED **Solution:** Check credentials: ServiceAccount Bearer token for config APIs, X-IK-ClientKey for execution ### 404: NOT_FOUND **Solution:** Verify the knowledge query ID exists and belongs to your project --- Source: https://developer.indykite.com/resources/ciq-2 --- # ContX IQ: Filter Data Using IN Array Operator with User Subject > Query Contract nodes using the IN operator to filter by array values in policy conditions. Demonstrates how to check if a value exists within an array property during authorization. **Category:** ContX IQ **API:** ContX IQ **Tags:** ContX IQ Policy, ContX IQ Query, ContX IQ Execution, IN Operator, Array Filter, User Subject **Last Updated:** 2026-03-20 **OpenAPI Endpoints:** /capture/v1/nodes, /capture/v1/relationships, /configs/v1/authorization-policies, /configs/v1/knowledge-queries, /contx-iq/v1/execute **Related Guides:** /guides/guide-contx-iq, /guides/guide-sandbox ## Summary This example demonstrates using the IN operator for array-based filtering: What is the IN operator? The IN operator checks if a value exists within an array. Useful when authorization depends on membership in a list. Policy pattern: WHERE user.role IN ["admin", "manager", "viewer"] Operations demonstrated: 1. Authenticate user via token introspection 2. Policy evaluates user attributes against allowed arrays 3. Query returns Contract nodes matching the filter criteria Use cases: - Role-based access (check if user role is in allowed roles list) - Category filtering (check if item category is in permitted categories) - Multi-value attribute matching ## Use Case Scenario: Retrieve contracts that a user is authorized to view based on their roles. Graph structure: Person(Alice) -[ACCEPTED]-> Contract1 {type: "rental"} Person(Alice) -[ACCEPTED]-> Contract2 {type: "lease"} Policy logic: - User authenticates with access token - Token is introspected to extract user identity - Policy filters contracts where contract.type IN ["rental", "purchase"] - Only contracts matching the array filter are returned Query flow: 1. User token introspected -> Person(Alice) identified 2. Policy checks Contract types against allowed array 3. Contract1 returned (type "rental" is in array) 4. Contract2 filtered out (type "lease" not in array) ## Requirements Prerequisites: - ServiceAccount credentials: For creating policies and queries (Bearer token) - AppAgent credentials: For data ingestion and query execution (X-IK-ClientKey) - User access token: JWT for the Person node to be authenticated How to pass user token: - Header: Authorization: Bearer {user_access_token} - Token is introspected to identify the user and their attributes ## Steps Step 1: Ingest Graph Data - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: POST Person, Contract nodes and relationships - Result: Graph ready for array-filtered queries Step 2: Create Policy with IN Array Filter - Authentication: ServiceAccount credential (Bearer token) - Action: POST policy with: - Subject filter using token introspection - Resource filter using IN operator on array values - Result: Policy ID returned Step 3: Create Query for Contract Properties - Authentication: ServiceAccount credential (Bearer token) - Action: POST query that retrieves Contract properties - Query respects the IN array filter from the policy - Result: Query ID returned Step 4: Execute Query as Authenticated User - Authentication: AppAgent credential + User token (Bearer header) - Action: POST to /contx-iq/v1/execute - Result: Contract nodes matching the array filter returned ## Code Examples ### Step 1 Capture the nodes needed for this use case. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "alice_sub_value", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "alice@email.com" }, { "type": "given_name", "value": "Alice" }, { "type": "last_name", "value": "Smith" } ] }, { "external_id": "ryan", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "ryan@yahoo.co.uk" }, { "type": "given_name", "value": "ryan" }, { "type": "last_name", "value": "mushu" } ] }, { "external_id": "tilda", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "tilda@yahoo.co.uk" }, { "type": "given_name", "value": "tilda" }, { "type": "last_name", "value": "mushu" } ] }, { "external_id": "ct123", "type": "Contract", "properties": [ { "type": "category", "value": "Insurance" }, { "type": "status", "value": "Active" }, { "type": "signers", "value": [ "tilda" ] } ] }, { "external_id": "ct234", "type": "Contract", "properties": [ { "type": "category", "value": "Insurance" }, { "type": "status", "value": "Active" }, { "type": "signers", "value": [ "alice_sub_value", "ryan" ] } ] }, { "external_id": "ct985", "type": "Contract", "properties": [ { "type": "category", "value": "Insurance" }, { "type": "status", "value": "Active" }, { "type": "signers", "value": [ "alice_sub_value", "ryan", "tilda" ] } ] } ] } ``` Capture the relationships needed for this use case. **POST https://eu.api.indykite.com/capture/v1/relationships/** ```json { "relationships": [ { "source": { "external_id": "alice_sub_value", "type": "Person" }, "target": { "external_id": "ct985", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "alice_sub_value", "type": "Person" }, "target": { "external_id": "ct234", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "ryan", "type": "Person" }, "target": { "external_id": "ct985", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "ryan", "type": "Person" }, "target": { "external_id": "ct234", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "tilda", "type": "Person" }, "target": { "external_id": "ct985", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "tilda", "type": "Person" }, "target": { "external_id": "ct123", "type": "Contract" }, "type": "ACCEPTED" } ] } ``` ### Step 2 Create a CIQ Policy which designates the Subject node, the cypher, the nodes allowed to be upserted and the nodes allowed to be read. **policy.json** ```json { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "Person" }, "condition": { "cypher": "MATCH (subject:Person)-[r1:ACCEPTED]->(contract:Contract)", "filter": [ { "app": "app1", "operator": "AND", "operands": [ { "attribute": "subject.external_id", "operator": "IN", "value": "$idList" }, { "attribute": "subject.external_id", "operator": "IN", "value": "@contract.property.signers" } ] } ] }, "allowed_reads": { "nodes": [ "contract.*" ], "relationships": [ "r1.*" ] } } ``` Request to create the CIQ Policy configuration using REST. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"meta\":{\"policy_version\":\"1.0-ciq\"},\"subject\":{\"type\":\"Person\"},\"condition\":{\"cypher\":\"MATCH (subject:Person)-[r1:ACCEPTED]->(contract:Contract)\",\"filter\":[{\"app\":\"app1\",\"operator\":\"AND\",\"operands\":[{\"attribute\":\"subject.external_id\",\"operator\":\"IN\",\"value\":\"$idList\"},{\"attribute\":\"subject.external_id\",\"operator\":\"IN\",\"value\":\"@contract.property.signers\"}]}]},\"allowed_reads\":{\"nodes\":[\"contract.*\"],\"relationships\":[\"r1.*\"]}}", "status": "ACTIVE", "tags": [] } ``` Request to read the CIQ Policy configuration using REST. **policy_request.json** ```json { "id": "your_policy_configuration_gid" } ``` ### Step 3 Create a CIQ Query in the context of the policy to retrieve the designated properties. **knowledge_query.json** ```json { "nodes": [ "contract.external_id", "contract.property.signers", "contract.property.category" ] } ``` Request to create a CIQ Query configuration using REST. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "description of knowledge query", "display_name": "knowledge query name", "name": "knowledge-query-name", "policy_id": "your_policy_gid", "query": "{\"nodes\":[\"contract.external_id\",\"contract.property.signers\",\"contract.property.category\"]}", "status": "ACTIVE" } ``` Read the CIQ Query Configuration. **GET https://eu.api.indykite.com/configs/v1/knowledge-queries/{id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 4 Run a CIQ Execution to get the designated information. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "knowledge_query_gid", "input_params": { "idList": [ "alice_sub_value", "ryan" ] }, "page_token": 1 } ``` CIQ Execution response. **response.json** ```json { "data": [ { "nodes": { "contract.external_id": "ct234", "contract.property.category": "Insurance", "contract.property.signers": [ "alice_sub_value", "ryan" ] } }, { "nodes": { "contract.external_id": "ct985", "contract.property.category": "Insurance", "contract.property.signers": [ "alice_sub_name", "ryan", "tilda" ] } }, { "nodes": { "contract.external_id": "ct985", "contract.property.category": "Insurance", "contract.property.signers": [ "alice_sub_name", "ryan", "tilda" ] } } ] } ``` ## Common Errors ### 401: UNAUTHENTICATED **Solution:** Check credentials: ServiceAccount Bearer token for config APIs, X-IK-ClientKey for execution ### 404: NOT_FOUND **Solution:** Verify the query/policy ID exists and belongs to your project --- Source: https://developer.indykite.com/resources/ciq-20 --- # ContX IQ: Aggregate Data Using WHERE, WITH, and COUNT Operators > Demonstrates advanced Cypher query patterns including WHERE filtering, WITH for intermediate results, and COUNT for aggregation. Returns Person data with contract counts. **Category:** ContX IQ **API:** ContX IQ **Tags:** ContX IQ Policy, ContX IQ Query, ContX IQ Execution, Aggregation, COUNT, WHERE, WITH **Last Updated:** 2026-03-20 **OpenAPI Endpoints:** /capture/v1/nodes, /capture/v1/relationships, /configs/v1/authorization-policies, /configs/v1/knowledge-queries, /contx-iq/v1/execute **Related Guides:** /guides/guide-contx-iq, /guides/guide-sandbox ## Summary This example demonstrates aggregation and filtering in ContX IQ queries: Cypher operators demonstrated: 1. WHERE - Filter nodes based on conditions 2. WITH - Pass results between query parts (intermediate projection) 3. COUNT - Aggregate and count related nodes Query pattern: MATCH (person:Person)-[r:ACCEPTED]->(contract:Contract) WHERE person.status = "active" WITH person, COUNT(contract) AS contractCount RETURN person.name, contractCount Use cases: - Dashboard summaries (count related entities) - Filtered aggregations (count only matching items) - Multi-step queries with intermediate results ## Use Case Scenario: Generate a summary showing each Person and how many Contracts they have. Graph structure: Person(Alice) -[ACCEPTED]-> Contract1 Person(Alice) -[ACCEPTED]-> Contract2 Person(Alice) -[ACCEPTED]-> Contract3 Person(Bob) -[ACCEPTED]-> Contract4 Query flow: 1. User authenticates with access token 2. Token introspected -> User identified 3. Query matches Person nodes and their Contracts 4. WHERE filters to active persons (if applicable) 5. WITH creates intermediate result with COUNT 6. Returns: [{name: "Alice", contractCount: 3}, {name: "Bob", contractCount: 1}] Key concepts: - WITH acts as a "checkpoint" in the query, projecting results forward - COUNT aggregates across relationships - Filtering happens before or after WITH as needed ## Requirements Prerequisites: - ServiceAccount credentials: For creating policies and queries (Bearer token) - AppAgent credentials: For data ingestion and query execution (X-IK-ClientKey) - User access token: JWT for the authenticated user Required API access: - POST /capture/v1/nodes/ and /capture/v1/relationships/ (data ingestion) - POST /configs/v1/authorization-policies (create policy) - POST /configs/v1/knowledge-queries (create query) - POST /contx-iq/v1/execute (run query) ## Steps Step 1: Ingest Graph Data - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: POST Person and Contract nodes with ACCEPTED relationships - Result: Graph with persons and varying numbers of contracts Step 2: Create Aggregation Policy - Authentication: ServiceAccount credential (Bearer token) - Action: POST policy allowing: - READ on Person nodes - READ on Contract nodes (for counting) - Aggregation operations (COUNT) - Result: Policy ID returned Step 3: Create Aggregation Query - Authentication: ServiceAccount credential (Bearer token) - Action: POST query using: - WHERE for filtering conditions - WITH for intermediate projection - COUNT for aggregating contracts per person - Result: Query ID returned Step 4: Execute Aggregation Query - Authentication: AppAgent credential + User token (Bearer header) - Action: POST to /contx-iq/v1/execute - Result: Array of Person records with their contract counts ## Code Examples ### Step 1 Capture the nodes needed for this use case. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "alice", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "alice@email.com" }, { "type": "given_name", "value": "Alice" }, { "type": "last_name", "value": "Smith" } ] }, { "external_id": "ryan", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "ryan@yahoo.co.uk" }, { "type": "given_name", "value": "ryan" }, { "type": "last_name", "value": "mushu" } ] }, { "external_id": "tilda", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "tilda@yahoo.co.uk" }, { "type": "given_name", "value": "tilda" }, { "type": "last_name", "value": "mushu" } ] }, { "external_id": "cb123", "type": "PaymentMethod", "properties": [ { "type": "payment_name", "value": "Credit Card" } ] }, { "external_id": "kl123", "type": "PaymentMethod", "properties": [ { "type": "payment_name", "value": "Klarna" } ] }, { "external_id": "ct123", "type": "Contract", "properties": [ { "type": "category", "value": "Insurance" }, { "type": "status", "value": "Active" }, { "type": "number", "value": "hfgrten123", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "ct234", "type": "Contract", "properties": [ { "type": "category", "value": "Insurance" }, { "type": "status", "value": "Active" }, { "type": "number", "value": "hfgrten234", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "ct985", "type": "Contract", "properties": [ { "type": "category", "value": "Insurance" }, { "type": "status", "value": "Active" }, { "type": "number", "value": "hfgrten985", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "car1", "type": "Vehicle", "properties": [ { "type": "category", "value": "Car" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "rtfhcnvjt471" } ] }, { "external_id": "car2", "type": "Vehicle", "properties": [ { "type": "category", "value": "Car" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "kdcbfrt178" } ] }, { "external_id": "truck1", "type": "Vehicle", "properties": [ { "type": "category", "value": "Truck" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "sncnrkcldp" } ] }, { "external_id": "license1", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "AX123456", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "license2", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "OL123456", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "license3", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "VN123456", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "company1", "type": "Company", "properties": [ { "type": "name", "value": "Company1" }, { "type": "registration", "value": "256314523" } ] }, { "external_id": "company2", "type": "Company", "properties": [ { "type": "name", "value": "Company2" }, { "type": "registration", "value": "942365123" } ] }, { "external_id": "application1", "type": "Application", "properties": [ { "type": "name", "value": "Application" } ] }, { "external_id": "application2", "type": "Application", "properties": [ { "type": "name", "value": "Application2" } ] } ] } ``` Capture the relationships needed for this use case. **POST https://eu.api.indykite.com/capture/v1/relationships/** ```json { "relationships": [ { "source": { "external_id": "ryan", "type": "Person" }, "target": { "external_id": "cb123", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "tilda", "type": "Person" }, "target": { "external_id": "kl123", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "cb123", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "ct123", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "ct234", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "ct985", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "ct123", "type": "Contract" }, "target": { "external_id": "car1", "type": "Vehicle" }, "type": "COVERS" }, { "source": { "external_id": "ct985", "type": "Contract" }, "target": { "external_id": "car1", "type": "Vehicle" }, "type": "COVERS" }, { "source": { "external_id": "ct234", "type": "Contract" }, "target": { "external_id": "truck1", "type": "Vehicle" }, "type": "COVERS" }, { "source": { "external_id": "car1", "type": "Vehicle" }, "target": { "external_id": "license1", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "truck1", "type": "Vehicle" }, "target": { "external_id": "license2", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "car2", "type": "Vehicle" }, "target": { "external_id": "license3", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "company1", "type": "Company" }, "target": { "external_id": "car1", "type": "Vehicle" }, "type": "OWNS" }, { "source": { "external_id": "company1", "type": "Company" }, "target": { "external_id": "car2", "type": "Vehicle" }, "type": "OWNS" }, { "source": { "external_id": "company1", "type": "Company" }, "target": { "external_id": "truck1", "type": "Vehicle" }, "type": "OWNS" }, { "source": { "external_id": "application1", "type": "Application" }, "target": { "external_id": "company1", "type": "Company" }, "type": "HAS_AGREEMENT_WITH" }, { "source": { "external_id": "application2", "type": "Application" }, "target": { "external_id": "company1", "type": "Company" }, "type": "HAS_AGREEMENT_WITH" } ] } ``` ### Step 2 Create a CIQ Policy which designates the Subject node, the cypher, and the nodes allowed to be read. **policy.json** ```json { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "Person" }, "condition": { "cypher": "MATCH (subject:Person)-[r1:ACCEPTED]->(contract:Contract) WITH subject, COUNT(contract) AS numContracts", "filter": [ { "app": "app1", "operator": "AND", "operands": [ { "attribute": "subject.external_id", "operator": "=", "value": "$subjectId" }, { "attribute": "numContracts", "operator": ">", "value": 1 } ] } ] }, "allowed_reads": { "nodes": [ "subject.*" ], "aggregate_values": [ "numContracts" ] } } ``` Request to create the CIQ Policy configuration using REST. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"meta\":{\"policy_version\":\"1.0-ciq\"},\"subject\":{\"type\":\"Person\"},\"condition\":{\"cypher\":\"MATCH (subject:Person)-[r1:ACCEPTED]->(contract:Contract) WITH subject, COUNT(contract) AS numContracts\",\"filter\":[{\"app\":\"app1\",\"operator\":\"AND\",\"operands\":[{\"attribute\":\"subject.external_id\",\"operator\":\"=\",\"value\":\"$subjectId\"},{\"attribute\":\"numContracts\",\"operator\":\">\",\"value\":1}]}]},\"allowed_reads\":{\"nodes\":[\"subject.*\"],\"aggregate_values\":[\"numContracts\"]}}", "status": "ACTIVE", "tags": [] } ``` Request to read the CIQ Policy configuration using REST. **policy_request.json** ```json { "id": "your_policy_configuration_gid" } ``` ### Step 3 Create a CIQ Query in the context of the policy to retrieve the designated properties. **knowledge_query.json** ```json { "nodes": [ "subject.external_id" ], "aggregate_values": [ "numContracts" ] } ``` Request to create a CIQ Query configuration using REST. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "description of knowledge query", "display_name": "knowledge query name", "name": "knowledge-query-name", "policy_id": "your_policy_gid", "query": "{\"nodes\":[\"subject.external_id\"],\"aggregate_values\":[\"numContracts\"]}", "status": "ACTIVE" } ``` Read the CIQ Query Configuration. **GET https://eu.api.indykite.com/configs/v1/knowledge-queries/{id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 4 Run a CIQ Execution to get the designated information. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "knowledge_query_gid", "input_params": { "subjectId": "alice" }, "page_token": 1 } ``` CIQ Execution response. **response.json** ```json { "data": [ { "nodes": { "subject.external_id": "alice" }, "aggregate_values": { "numContracts": 3 } } ] } ``` ## Common Errors ### 401: UNAUTHENTICATED **Solution:** Check credentials: ServiceAccount Bearer token for config APIs, X-IK-ClientKey for execution ### 404: NOT_FOUND **Solution:** Verify the query/policy ID exists and belongs to your project --- Source: https://developer.indykite.com/resources/ciq-21 --- # ContX IQ: Check Resource Availability Using OPTIONAL MATCH and CASE Expression > Demonstrates OPTIONAL MATCH for pattern matching that doesn't fail when no match exists, combined with CASE expressions for conditional logic. Example: Check if a subdomain is available for registration. **Category:** ContX IQ **API:** ContX IQ **Tags:** ContX IQ Policy, ContX IQ Query, ContX IQ Execution, OPTIONAL MATCH, CASE, Availability Check, _Application Subject **Last Updated:** 2026-03-20 **OpenAPI Endpoints:** /capture/v1/nodes, /capture/v1/relationships, /configs/v1/authorization-policies, /configs/v1/knowledge-queries, /contx-iq/v1/execute **Related Guides:** /guides/guide-contx-iq, /guides/guide-sandbox ## Summary This example demonstrates OPTIONAL MATCH with CASE expressions: What is OPTIONAL MATCH? Unlike regular MATCH, OPTIONAL MATCH continues query execution even when no match is found (returns null instead of failing). What is CASE expression? Conditional logic in Cypher that returns different values based on conditions. Combined pattern: OPTIONAL MATCH (tenant:Tenant)-[:HAS]->(prop {type: "subdomain", value: $subdomain}) RETURN CASE WHEN tenant IS NULL THEN true ELSE false END AS is_available Use case: Check if a subdomain is available (not already registered). ## Use Case Scenario: Before registering a new tenant subdomain, check if it's already taken. Graph structure: Tenant(Acme) -[HAS]-> Property(type: "subdomain", value: "acme.example.com") Tenant(Beta) -[HAS]-> Property(type: "subdomain", value: "beta.example.com") Query input: $subdomain = "newsite.example.com" Query flow: 1. _Application authenticates (using $_appId auto-filter) 2. OPTIONAL MATCH searches for Tenant with matching subdomain property 3. No match found -> tenant = null 4. CASE evaluates: tenant IS NULL -> return true (available) 5. Result: {is_subdomain_available: true} If subdomain existed: - Input: $subdomain = "acme.example.com" - Tenant(Acme) matched - CASE evaluates: tenant IS NOT NULL -> return false - Result: {is_subdomain_available: false} Key concept: _Application as subject: - Uses $_appId filter (auto-populated with Application's external_id) - No user token required - service-to-service pattern ## Requirements Prerequisites: - ServiceAccount credentials: For creating policies and queries (Bearer token) - AppAgent credentials: For data ingestion and query execution (X-IK-ClientKey) _Application as subject: - $_appId is automatically populated when using Application credentials - No user authentication needed for this pattern Required API access: - POST /capture/v1/nodes/ (capture Tenant and Property nodes) - POST /configs/v1/authorization-policies (create policy) - POST /configs/v1/knowledge-queries (create query) - POST /contx-iq/v1/execute (run availability check) ## Steps Step 1: Ingest Tenant and Property Nodes - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: POST Tenant nodes with subdomain Property nodes - Result: Graph with existing subdomains registered Step 2: Create Availability Check Policy - Authentication: ServiceAccount credential (Bearer token) - Action: POST policy with: - Subject filter: $_appId (auto-populated for _Application) - Aggregation permission for CASE expression results - Result: Policy ID returned Step 3: Create Availability Check Query - Authentication: ServiceAccount credential (Bearer token) - Action: POST query using: - OPTIONAL MATCH to find existing subdomain (may return null) - CASE expression to convert match/no-match to boolean - Parameter: $subdomain for the subdomain to check - Result: Query ID returned Step 4: Execute Availability Check - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: POST to /contx-iq/v1/execute with subdomain parameter - Result: {is_subdomain_available: true/false} ## Code Examples ### Step 1 Capture the nodes needed for this use case. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "tenant1", "is_identity": false, "type": "Tenant", "properties": [ { "type": "name", "value": "Tenant1" }, { "type": "subdomain", "value": "whocares" } ] }, { "external_id": "tenant2", "is_identity": false, "type": "Tenant", "properties": [ { "type": "name", "value": "Tenant2" }, { "type": "subdomain", "value": "whatabout" } ] }, { "external_id": "tenant3", "is_identity": false, "type": "Tenant", "properties": [ { "type": "name", "value": "Tenant3" } ] } ] } ``` ### Step 2 Create a CIQ Policy which designates the Subject node, the cypher, and the nodes allowed to be agregated. **policy.json** ```json { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "_Application" }, "condition": { "cypher": "MATCH (subject:_Application) WHERE subject.external_id = $_appId WITH subject OPTIONAL MATCH (tenant:Tenant)-[:HAS]->(subdomain:Property) WHERE subdomain.type = 'subdomain' AND subdomain.value = $subdomain WITH subject, CASE tenant WHEN tenant THEN false ELSE true END AS is_subdomain_available", "filter": [] }, "allowed_reads": { "nodes": [ "subject.*", "tenant.*" ], "aggregate_values": [ "is_subdomain_available" ] } } ``` Request to create the CIQ Policy configuration using REST. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"meta\":{\"policy_version\":\"1.0-ciq\"},\"subject\":{\"type\":\"_Application\"},\"condition\":{\"cypher\":\"MATCH (subject:_Application) WHERE subject.external_id = $_appId WITH subject OPTIONAL MATCH (tenant:Tenant)-[:HAS]->(subdomain:Property) WHERE subdomain.type = 'subdomain' AND subdomain.value = $subdomain WITH subject, CASE tenant WHEN tenant THEN false ELSE true END AS is_subdomain_available\",\"filter\":[]},\"allowed_reads\":{\"nodes\":[\"subject.*\",\"tenant.*\"],\"aggregate_values\":[\"is_subdomain_available\"]}}", "status": "ACTIVE", "tags": [] } ``` Request to read the CIQ Policy configuration using REST. **policy_request.json** ```json { "id": "your_policy_configuration_gid" } ``` ### Step 3 Create a CIQ Query in the context of the policy to retrieve the designated properties. **knowledge_query.json** ```json { "aggregate_values": [ "is_subdomain_available" ] } ``` Request to create a CIQ Query configuration using REST. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "description of knowledge query", "display_name": "knowledge query name", "name": "knowledge-query-name", "policy_id": "your_policy_gid", "query": "{\"aggregate_values\":[\"is_subdomain_available\"]}", "status": "ACTIVE" } ``` Read the CIQ Query Configuration. **GET https://eu.api.indykite.com/configs/v1/knowledge-queries/{id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 4 Run a CIQ Execution to get the designated information. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "knowledge_query_gid", "input_params": { "subdomain": "whocares" }, "page_token": 1 } ``` CIQ Execution response. **response.json** ```json { "data": [ { "aggregate_values": { "is_subdomain_available": false } } ] } ``` ## Common Errors ### 401: UNAUTHENTICATED **Solution:** Check credentials: ServiceAccount Bearer token for config APIs, X-IK-ClientKey for execution ### 404: NOT_FOUND **Solution:** Verify the query/policy ID exists and belongs to your project --- Source: https://developer.indykite.com/resources/ciq-22 --- # ContX IQ: Paginated Query Execution with page_token and page_size > Walk a large CIQ result set across multiple /contx-iq/v1/execute calls using the page_token and page_size request fields. Demonstrates client-side pagination of an authorized read query. **Category:** ContX IQ **API:** ContX IQ **Tags:** ContX IQ Execution, Pagination, page_token, page_size, Large Result Sets **Last Updated:** 2026-05-20 **OpenAPI Endpoints:** /capture/v1/nodes, /capture/v1/relationships, /configs/v1/authorization-policies, /configs/v1/knowledge-queries, /contx-iq/v1/execute **Related Guides:** /guides/guide-contx-iq ## Summary This example demonstrates pagination on the CIQ Execute endpoint. Request fields: - page_token (integer, optional): Page index. Any value below 1 returns page 1. - page_size (integer, optional): Result set size. Default is 100. Pagination model: - The client increments page_token by page_size between calls (page 1 -> token 0 or 1, page 2 -> token = page_size, page 3 -> token = 2 * page_size, ...). - When fewer than page_size records come back, you've hit the last page. - Pagination applies to the records inside data[]; partial filters and authorization are re-evaluated on every call. When to use: - Knowledge Queries that may return hundreds or thousands of nodes/relationships. - Streaming-style consumers that prefer fixed-size chunks. ## Use Case Scenario: A back-office UI lists every car license alice owns, two rows at a time. Graph structure: - Person(alice) -[OWNS]-> Car(kitt) -[HAS]-> LicenseNumber("KITT 0001") - Person(alice) -[OWNS]-> Car(caddilacv16) -[HAS]-> LicenseNumber("CADV16-007") - Person(alice) -[OWNS]-> Car(skodaOctavia) -[HAS]-> LicenseNumber("OCT-2021-XX") Walk: 1. Call /execute with page_token=0, page_size=2 -> returns kitt + caddilacv16. 2. Call /execute with page_token=2, page_size=2 -> returns skodaOctavia (one record => last page). 3. The UI knows there are no more rows and stops paging. Tip: Use page_size to bound memory and latency, not to limit returned data. Authorization is applied to every record on every call. ## Requirements Prerequisites: - ServiceAccount credentials: For creating the policy and Knowledge Query. - AppAgent credentials: For ingesting the graph and executing the query. - Bearer token for the subject (alice) when subject type is not _Application. Required API access: - POST /capture/v1/nodes/ and /capture/v1/relationships/ - POST /configs/v1/authorization-policies - POST /configs/v1/knowledge-queries - POST /contx-iq/v1/execute (called once per page) ## Steps Step 1: Capture the Graph - Action: POST Person (alice), her Cars, and their LicenseNumber nodes, plus OWNS and HAS relationships. - Result: Three car/license rows for alice to page through. Step 2: Create a Read-Only Policy - Action: POST a policy authorizing the Person -> Car -> LicenseNumber traversal, filtered to the subject. - Result: Policy ID. Step 3: Create the Knowledge Query - Action: POST a Knowledge Query that reads car.external_id and ln.property.number. - Result: Knowledge Query ID. Step 4: Execute Page 1 - Authentication: AppAgent credential. - Action: POST to /contx-iq/v1/execute with page_token=0 and page_size=2. - Result: First 2 records. Step 5: Execute Page 2 - Action: POST to /contx-iq/v1/execute with page_token=2 and page_size=2. - Result: Remaining record(s). Fewer than page_size means this is the last page. ## Code Examples ### Step 1 Capture the Person (alice), her Cars, and their LicenseNumber nodes. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "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. **POST https://eu.api.indykite.com/capture/v1/relationships/** ```json { "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 2 Read-only CIQ Policy authorizing the alice -> Car -> LicenseNumber traversal (subject filtered by subject_external_id). **policy.json** ```json { "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_reads": { "nodes": [ "car", "car.*", "ln", "ln.*" ] } } ``` Request to create the CIQ Policy configuration using REST. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "Authorize the Person -> Car -> LicenseNumber traversal so a read Knowledge Query can return each car and its license number for the subject.", "display_name": "policy - person owns cars with license numbers", "name": "policy-person-owns-car-licenses", "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_reads\":{\"nodes\":[\"car\",\"car.*\",\"ln\",\"ln.*\"]}}", "status": "ACTIVE", "tags": [] } ``` ### Step 3 Knowledge Query that returns car.external_id and ln.property.number with no filter - pagination will limit how many records come back per call. **knowledge_query.json** ```json { "nodes": [ "car.external_id", "ln.property.number" ] } ``` Request to create the Knowledge Query. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "Return every car license-number tuple this subject is authorized to see. Designed for pagination via page_token / page_size on /contx-iq/v1/execute.", "display_name": "knowledge query - paginated car licenses", "name": "kq-paginated-licenses", "policy_id": "your_policy_gid", "query": "{\"nodes\":[\"car.external_id\",\"ln.property.number\"]}", "status": "ACTIVE" } ``` ### Step 4 Page 1: page_token=0, page_size=2 - returns the first two records. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "your_query_gid_or_name", "input_params": { "subject_external_id": "alice" }, "page_token": 0, "page_size": 2 } ``` Page 1 response - 2 records. **response_page_1.json** ```json { "data": [ { "nodes": { "car.external_id": "kitt", "ln.property.number": "KITT 0001" } }, { "nodes": { "car.external_id": "caddilacv16", "ln.property.number": "CADV16-007" } } ] } ``` ### Step 5 Page 2: advance page_token to 2 (page_size). Same page_size keeps result chunks consistent. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "your_query_gid_or_name", "input_params": { "subject_external_id": "alice" }, "page_token": 2, "page_size": 2 } ``` Page 2 response - 1 record. Fewer than page_size, so this is the last page. **response_page_2.json** ```json { "data": [ { "nodes": { "car.external_id": "skodaOctavia", "ln.property.number": "OCT-2021-XX" } } ] } ``` ## Common Errors ### 400: page_size must be a positive integer **Solution:** Use a positive integer for page_size. Omit the field to fall back to the default of 100. ### 401: UNAUTHENTICATED **Solution:** Each page is a fresh /contx-iq/v1/execute call and re-authorizes. Re-send X-IK-ClientKey and the bearer token on every page. --- Source: https://developer.indykite.com/resources/ciq-23 --- # ContX IQ: Grant Unrestricted READ Access to a Node Category > Create a policy that allows any authenticated request to READ all nodes of a specific type (e.g., all LicenseNumber nodes) without requiring relationship-based authorization. **Category:** ContX IQ **API:** ContX IQ **Tags:** ContX IQ Policy, ContX IQ Query, ContX IQ Execution, Unrestricted Read, Category Access **Last Updated:** 2026-03-20 **OpenAPI Endpoints:** /capture/v1/nodes, /capture/v1/relationships, /configs/v1/authorization-policies, /configs/v1/knowledge-queries, /contx-iq/v1/execute **Related Guides:** /guides/guide-contx-iq, /guides/guide-sandbox ## Summary This example demonstrates how to create an "allow all" READ policy for a node category: 1. The policy grants READ access to ALL LicenseNumber nodes 2. No relationship path is required between the subject and the data 3. No input filters are needed - the policy matches all nodes of the specified type Use case: Public or semi-public data that any authenticated application can access. Contrast with ciq-basic: In ciq-basic, a Person can only read LicenseNumbers they're connected to via contracts. Here, any authenticated request can read ALL LicenseNumbers. ## Use Case Scenario: Your system has LicenseNumber data that should be readable by any authenticated application. Policy behavior: - Subject: _Application (any authenticated application) - Action: READ - Target: ALL LicenseNumber nodes (no relationship path required) - Filter: None (matches all LicenseNumbers) Example: A vehicle lookup service where any authenticated API client can query any license number. Security note: Use this pattern only for data that should be broadly accessible. For sensitive data, use relationship-based policies (see ciq-basic). ## Requirements Prerequisites: - ServiceAccount credentials: For creating policies and queries (Bearer token) - AppAgent credentials: For data ingestion and query execution (X-IK-ClientKey) Required API access: - POST /capture/v1/nodes/ and /capture/v1/relationships/ (data ingestion) - POST /configs/v1/authorization-policies (create policy) - POST /configs/v1/knowledge-queries (create query) - POST /contx-iq/v1/execute (run query) ## Steps Step 1: Ingest Graph Data - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: POST nodes and relationships - Result: LicenseNumber nodes (and supporting graph) created Step 2: Create "Allow All Read" Policy - Authentication: ServiceAccount credential (Bearer token) - Action: POST policy with no relationship path requirements - Key detail: Uses $_appId filter to match the calling Application, but grants access to ALL LicenseNumber nodes - Result: Policy ID returned Step 3: Create Query - Authentication: ServiceAccount credential (Bearer token) - Action: POST query that returns all LicenseNumber nodes - Result: Query ID returned Step 4: Execute Query - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: POST to /contx-iq/v1/execute - Result: Array of ALL LicenseNumber values in the graph Step 5: Cleanup - Action: DELETE query and policy configurations ## Code Examples ### Step 1a Capture nodes including LicenseNumber nodes that will be queried. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "alice", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "alice@email.com" }, { "type": "given_name", "value": "Alice" }, { "type": "last_name", "value": "Smith" } ] }, { "external_id": "ryan", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "ryan@yahoo.co.uk" }, { "type": "given_name", "value": "ryan" }, { "type": "last_name", "value": "mushu" } ] }, { "external_id": "tilda", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "tilda@yahoo.co.uk" }, { "type": "given_name", "value": "tilda" }, { "type": "last_name", "value": "mushu" } ] }, { "external_id": "cb123", "type": "PaymentMethod", "properties": [ { "type": "payment_name", "value": "Credit Card" } ] }, { "external_id": "kl123", "type": "PaymentMethod", "properties": [ { "type": "payment_name", "value": "Klarna" } ] }, { "external_id": "ct123", "type": "Contract", "properties": [ { "type": "category", "value": "Insurance" }, { "type": "status", "value": "Active" }, { "type": "number", "value": "hfgrten123", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "ct234", "type": "Contract", "properties": [ { "type": "category", "value": "Insurance" }, { "type": "status", "value": "Active" }, { "type": "number", "value": "hfgrten234", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "ct985", "type": "Contract", "properties": [ { "type": "category", "value": "Insurance" }, { "type": "status", "value": "Active" }, { "type": "number", "value": "hfgrten985", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "car1", "type": "Vehicle", "properties": [ { "type": "category", "value": "Car" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "rtfhcnvjt471" } ] }, { "external_id": "car2", "type": "Vehicle", "properties": [ { "type": "category", "value": "Car" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "kdcbfrt178" } ] }, { "external_id": "truck1", "type": "Vehicle", "properties": [ { "type": "category", "value": "Truck" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "sncnrkcldp" } ] }, { "external_id": "license1", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "AX123456", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "license2", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "OL123456", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "license3", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "VN123456", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "company1", "type": "Company", "properties": [ { "type": "name", "value": "Company1" }, { "type": "registration", "value": "256314523" } ] }, { "external_id": "company2", "type": "Company", "properties": [ { "type": "name", "value": "Company2" }, { "type": "registration", "value": "942365123" } ] }, { "external_id": "application1", "type": "Application", "properties": [ { "type": "name", "value": "Application" } ] }, { "external_id": "application2", "type": "Application", "properties": [ { "type": "name", "value": "Application2" } ] } ] } ``` ### Step 1b Capture relationships (optional for this use case since no relationship path is required for authorization). **POST https://eu.api.indykite.com/capture/v1/relationships/** ```json { "relationships": [ { "source": { "external_id": "ryan", "type": "Person" }, "target": { "external_id": "cb123", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "tilda", "type": "Person" }, "target": { "external_id": "kl123", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "cb123", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "ryan", "type": "Person" }, "target": { "external_id": "ct123", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "tilda", "type": "Person" }, "target": { "external_id": "ct234", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "ct985", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "ct123", "type": "Contract" }, "target": { "external_id": "car1", "type": "Vehicle" }, "type": "COVERS" }, { "source": { "external_id": "ct985", "type": "Contract" }, "target": { "external_id": "car1", "type": "Vehicle" }, "type": "COVERS" }, { "source": { "external_id": "ct234", "type": "Contract" }, "target": { "external_id": "truck1", "type": "Vehicle" }, "type": "COVERS" }, { "source": { "external_id": "car1", "type": "Vehicle" }, "target": { "external_id": "license1", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "truck1", "type": "Vehicle" }, "target": { "external_id": "license2", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "car2", "type": "Vehicle" }, "target": { "external_id": "license3", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "company1", "type": "Company" }, "target": { "external_id": "car1", "type": "Vehicle" }, "type": "OWNS" }, { "source": { "external_id": "company1", "type": "Company" }, "target": { "external_id": "car2", "type": "Vehicle" }, "type": "OWNS" }, { "source": { "external_id": "company1", "type": "Company" }, "target": { "external_id": "truck1", "type": "Vehicle" }, "type": "OWNS" }, { "source": { "external_id": "application1", "type": "Application" }, "target": { "external_id": "company1", "type": "Company" }, "type": "HAS_AGREEMENT_WITH" }, { "source": { "external_id": "application2", "type": "Application" }, "target": { "external_id": "company1", "type": "Company" }, "type": "HAS_AGREEMENT_WITH" } ] } ``` ### Step 2a Policy JSON granting READ access to ALL LicenseNumber nodes. No relationship path defined - any authenticated _Application can read any LicenseNumber. **policy.json** ```json { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "_Application" }, "condition": { "cypher": "MATCH (subject:_Application) MATCH (p:Person) MATCH (ln:LicenseNumber)", "filter": [ { "attribute": "subject.external_id", "operator": "=", "value": "$_appId" } ] }, "allowed_reads": { "nodes": [ "ln.property.*" ] } } ``` ### Step 2b POST request to create the unrestricted read policy. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"meta\":{\"policy_version\":\"1.0-ciq\"},\"subject\":{\"type\":\"_Application\"},\"condition\":{\"cypher\":\"MATCH (subject:_Application) MATCH (p:Person) MATCH (ln:LicenseNumber)\",\"filter\":[{\"attribute\":\"subject.external_id\",\"operator\":\"=\",\"value\":\"$_appId\"}]},\"allowed_reads\":{\"nodes\":[\"ln.property.*\"]}}", "status": "ACTIVE", "tags": [] } ``` ### Step 2c GET request to verify the policy was created. **GET https://eu.api.indykite.com/configs/v1/authorization-policies/{policy_id}** ```json { "id": "your_policy_configuration_gid" } ``` ### Step 3a Query JSON that returns all LicenseNumber nodes. Authorization is satisfied by the allow-all policy. **knowledge_query.json** ```json { "nodes": [ "ln.property.number" ] } ``` ### Step 3b POST request to create the query. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "description of knowledge query", "display_name": "knowledge query name", "name": "knowledge-query-name", "policy_id": "your_policy_gid", "query": "{\"nodes\":[\"ln.property.number\"]}", "status": "ACTIVE" } ``` ### Step 3c GET request to verify the query was created. **GET https://eu.api.indykite.com/configs/v1/knowledge-queries/{query_id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 4a Execute the query. Returns ALL LicenseNumber values since the policy grants unrestricted read access. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "knowledge_query_gid", "input_params": {} } ``` ### Step 4b Response containing all LicenseNumber values in the graph. **response.json** ```json { "data": [ { "nodes": { "ln.property.number": "AX123456" } }, { "nodes": { "ln.property.number": "AX123456" } }, { "nodes": { "ln.property.number": "AX123456" } }, { "nodes": { "ln.property.number": "OL123456" } }, { "nodes": { "ln.property.number": "OL123456" } }, { "nodes": { "ln.property.number": "OL123456" } }, { "nodes": { "ln.property.number": "VN123456" } }, { "nodes": { "ln.property.number": "VN123456" } }, { "nodes": { "ln.property.number": "VN123456" } } ] } ``` ### Step 5a DELETE request to remove the query. **DELETE https://eu.api.indykite.com/configs/v1/knowledge-queries/{query_id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 5b DELETE request to remove the policy. **DELETE https://eu.api.indykite.com/configs/v1/authorization-policies/{policy_id}** ```json { "id": "your_policy_configuration_gid" } ``` ## Common Errors ### 401: UNAUTHENTICATED **Solution:** Check credentials: ServiceAccount Bearer token for config APIs, X-IK-ClientKey for execution ### 404: NOT_FOUND **Solution:** Verify the query/policy ID exists and belongs to your project --- Source: https://developer.indykite.com/resources/ciq-3 --- # ContX IQ: Create Nodes and Relationships via Authorized Write Query > Demonstrates how to create new Contract nodes and relationships (COVERS, ACCEPTED) through an authorized ContX IQ write query. The Application can only create contracts for vehicles owned by companies it has agreements with. **Category:** ContX IQ **API:** ContX IQ **Tags:** ContX IQ Policy, ContX IQ Query, ContX IQ Execution, Write Operations, Node Creation, Relationship Creation **Last Updated:** 2026-03-20 **OpenAPI Endpoints:** /capture/v1/nodes, /capture/v1/relationships, /configs/v1/authorization-policies, /configs/v1/knowledge-queries, /contx-iq/v1/execute **Related Guides:** /guides/guide-contx-iq, /guides/guide-sandbox ## Summary This example demonstrates authorized WRITE operations in ContX IQ: Phase 1 - Link Application to Company: 1. Create policy allowing _Application to create HAS_AGREEMENT_WITH relationships 2. Execute query to link Application to Company1 Phase 2 - Create Contract with Relationships: 3. Create policy allowing Contract node creation and COVERS/ACCEPTED relationships 4. Execute write query with parameters to create: - New Contract node (with parameterized external_id and number) - COVERS relationship: Contract -> Vehicle - ACCEPTED relationship: Person -> Contract Authorization constraint: The Application can only create contracts for vehicles owned by companies it has an agreement with. ## Use Case Scenario: Your application needs to create new rental contracts programmatically. Business rule: Contracts can only be created for vehicles owned by companies your application has agreements with. Write operations performed: 1. CREATE Contract node (with properties: external_id, number) 2. CREATE COVERS relationship (Contract -> Vehicle) 3. CREATE ACCEPTED relationship (Person -> Contract) Graph change: Before: Person(Ryan), Vehicle(Car1), Company1 exist separately After: Person(Ryan) -[ACCEPTED]-> Contract(New) -[COVERS]-> Vehicle(Car1) Key feature: The external_id and number for the Contract are passed as query parameters at execution time. ## Requirements Prerequisites: - ServiceAccount credentials: For creating policies and queries (Bearer token) - AppAgent credentials: For data ingestion and query execution (X-IK-ClientKey) Required API access: - POST /capture/v1/nodes/ and /capture/v1/relationships/ (initial data) - POST /configs/v1/authorization-policies (create policies) - POST /configs/v1/knowledge-queries (create queries) - POST /contx-iq/v1/execute (run queries) ## Steps Step 1: Ingest Initial Graph Data - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: POST nodes (Person, Vehicle, Company) and relationships - Result: Base graph structure ready for contract creation Step 2: Create Application Linking Policy - Authentication: ServiceAccount credential (Bearer token) - Action: POST policy allowing _Application to create HAS_AGREEMENT_WITH relationships - Result: Policy ID returned Step 3: Create Application Linking Query - Authentication: ServiceAccount credential (Bearer token) - Action: POST write query that creates Application -> Company relationship - Result: Query ID returned Step 4: Execute Application Linking - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: Execute the linking query - Result: _Application -[HAS_AGREEMENT_WITH]-> Company1 created Step 5: Create Contract Write Policy - Authentication: ServiceAccount credential (Bearer token) - Action: POST policy with allowed_upserts defining: - allowed_upserts.nodes: Can create Contract nodes - allowed_upserts.relationships: Can create COVERS and ACCEPTED relationships - Result: Policy ID returned Step 6: Create Contract Write Query - Authentication: ServiceAccount credential (Bearer token) - Action: POST parameterized query that creates Contract node and relationships - Parameters: $contractId, $contractNumber, $personEmail, $vehicleLicense - Result: Query ID returned Step 7: Execute Contract Creation - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: Execute with parameter values (e.g., contractId="C001", contractNumber="12345") - Result: New Contract node and relationships created in the graph Step 8: Cleanup - Action: DELETE queries and policies ## Code Examples ### Step 1a Capture base nodes: Person, Company, Vehicle, LicenseNumber. These will be linked by the contract created later. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "alice", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "alice@email.com" }, { "type": "given_name", "value": "Alice" }, { "type": "last_name", "value": "Smith" } ] }, { "external_id": "ryan", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "ryan@yahoo.co.uk" }, { "type": "given_name", "value": "ryan" }, { "type": "last_name", "value": "mushu" } ] }, { "external_id": "tilda", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "tilda@yahoo.co.uk" }, { "type": "given_name", "value": "tilda" }, { "type": "last_name", "value": "mushu" } ] }, { "external_id": "cb123", "type": "PaymentMethod", "properties": [ { "type": "payment_name", "value": "Credit Card" } ] }, { "external_id": "kl123", "type": "PaymentMethod", "properties": [ { "type": "payment_name", "value": "Klarna" } ] }, { "external_id": "ct123", "type": "Contract", "properties": [ { "type": "category", "value": "Insurance" }, { "type": "status", "value": "Active" }, { "type": "number", "value": "hfgrten123", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "ct234", "type": "Contract", "properties": [ { "type": "category", "value": "Insurance" }, { "type": "status", "value": "Active" }, { "type": "number", "value": "hfgrten234", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "ct985", "type": "Contract", "properties": [ { "type": "category", "value": "Insurance" }, { "type": "status", "value": "Active" }, { "type": "number", "value": "hfgrten985", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "car1", "type": "Vehicle", "properties": [ { "type": "category", "value": "Car" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "rtfhcnvjt471" } ] }, { "external_id": "car2", "type": "Vehicle", "properties": [ { "type": "category", "value": "Car" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "kdcbfrt178" } ] }, { "external_id": "truck1", "type": "Vehicle", "properties": [ { "type": "category", "value": "Truck" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "sncnrkcldp" } ] }, { "external_id": "license1", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "AX123456", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "license2", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "OL123456", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "license3", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "VN123456", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "company1", "type": "Company", "properties": [ { "type": "name", "value": "Company1" }, { "type": "registration", "value": "256314523" } ] }, { "external_id": "company2", "type": "Company", "properties": [ { "type": "name", "value": "Company2" }, { "type": "registration", "value": "942365123" } ] }, { "external_id": "application1", "type": "Application", "properties": [ { "type": "name", "value": "Application" } ] }, { "external_id": "application2", "type": "Application", "properties": [ { "type": "name", "value": "Application2" } ] } ] } ``` ### Step 1b Capture initial relationships (Company OWNS Vehicle, Vehicle HAS LicenseNumber). **POST https://eu.api.indykite.com/capture/v1/relationships/** ```json { "relationships": [ { "source": { "external_id": "ryan", "type": "Person" }, "target": { "external_id": "cb123", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "tilda", "type": "Person" }, "target": { "external_id": "kl123", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "cb123", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "ryan", "type": "Person" }, "target": { "external_id": "ct123", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "tilda", "type": "Person" }, "target": { "external_id": "ct234", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "ct985", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "ct123", "type": "Contract" }, "target": { "external_id": "car1", "type": "Vehicle" }, "type": "COVERS" }, { "source": { "external_id": "ct985", "type": "Contract" }, "target": { "external_id": "car1", "type": "Vehicle" }, "type": "COVERS" }, { "source": { "external_id": "ct234", "type": "Contract" }, "target": { "external_id": "truck1", "type": "Vehicle" }, "type": "COVERS" }, { "source": { "external_id": "car1", "type": "Vehicle" }, "target": { "external_id": "license1", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "truck1", "type": "Vehicle" }, "target": { "external_id": "license2", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "car2", "type": "Vehicle" }, "target": { "external_id": "license3", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "company1", "type": "Company" }, "target": { "external_id": "car1", "type": "Vehicle" }, "type": "OWNS" }, { "source": { "external_id": "company1", "type": "Company" }, "target": { "external_id": "car2", "type": "Vehicle" }, "type": "OWNS" }, { "source": { "external_id": "company1", "type": "Company" }, "target": { "external_id": "truck1", "type": "Vehicle" }, "type": "OWNS" }, { "source": { "external_id": "application1", "type": "Application" }, "target": { "external_id": "company1", "type": "Company" }, "type": "HAS_AGREEMENT_WITH" }, { "source": { "external_id": "application2", "type": "Application" }, "target": { "external_id": "company1", "type": "Company" }, "type": "HAS_AGREEMENT_WITH" } ] } ``` ### Step 2a Policy JSON allowing _Application to CREATE HAS_AGREEMENT_WITH relationships to Company nodes. **policy.json** ```json { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "_Application" }, "condition": { "cypher": "MATCH (subject:_Application) MATCH (company:Company)-[r2:OWNS]->(vehicle:Vehicle)", "filter": [ { "operator": "AND", "operands": [ { "attribute": "subject.external_id", "operator": "=", "value": "$_appId" }, { "attribute": "company.external_id", "operator": "=", "value": "$companyID" } ] } ] }, "allowed_upserts": { "relationships": { "relationship_types": [ { "type": "HAS_AGREEMENT_WITH", "source_node_label": "_Application", "target_node_label": "Company" } ] } }, "allowed_reads": { "nodes": [ "company.*", "subject.*" ] } } ``` ### Step 2b POST request to create the application linking policy. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"meta\":{\"policy_version\":\"1.0-ciq\"},\"subject\":{\"type\":\"_Application\"},\"condition\":{\"cypher\":\"MATCH (subject:_Application) MATCH (company:Company)-[r2:OWNS]->(vehicle:Vehicle)\",\"filter\":[{\"operator\":\"AND\",\"operands\":[{\"attribute\":\"subject.external_id\",\"operator\":\"=\",\"value\":\"$_appId\"},{\"attribute\":\"company.external_id\",\"operator\":\"=\",\"value\":\"$companyID\"}]}]},\"allowed_upserts\":{\"relationships\":{\"relationship_types\":[{\"type\":\"HAS_AGREEMENT_WITH\",\"source_node_label\":\"_Application\",\"target_node_label\":\"Company\"}]}},\"allowed_reads\":{\"nodes\":[\"company.*\",\"subject.*\"]}}", "status": "ACTIVE", "tags": [] } ``` ### Step 2c GET request to verify the policy was created. **GET https://eu.api.indykite.com/configs/v1/authorization-policies/{policy_id}** ```json { "id": "your_policy_configuration_gid" } ``` ### Step 3a Write query JSON that creates _Application -[HAS_AGREEMENT_WITH]-> Company relationship. **knowledge_query.json** ```json { "nodes": [ "subject.external_id" ], "relationships": [ "r1" ], "upsert_relationships": [ { "name": "r1", "source": "subject", "target": "company", "type": "HAS_AGREEMENT_WITH" } ] } ``` ### Step 3b POST request to create the application linking query. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "description of knowledge query", "display_name": "knowledge query name", "name": "knowledge-query-name", "policy_id": "your_policy_gid", "query": "{\"nodes\":[\"subject.external_id\"],\"relationships\":[\"r1\"],\"upsert_relationships\":[{\"name\":\"r1\",\"source\":\"subject\",\"target\":\"company\",\"type\":\"HAS_AGREEMENT_WITH\"}]}", "status": "ACTIVE" } ``` ### Step 3c GET request to verify the query was created. **GET https://eu.api.indykite.com/configs/v1/knowledge-queries/{query_id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 4a Execute the linking query. Creates the relationship in the graph. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "ciq_query_gid", "input_params": { "companyID": "company1" } } ``` ### Step 4b Response confirming the HAS_AGREEMENT_WITH relationship was created. **response.json** ```json { "data": [ { "nodes": { "subject.external_id": "application_external_id" }, "relationships": { "r1": { "Id": 1152932499723124700, "ElementId": "5:3a2b09d5-2923-45d7-8453-8b1c698427b0:1152932499723124736", "StartId": 0, "StartElementId": "4:3a2b09d5-2923-45d7-8453-8b1c698427b0:0", "EndId": 15, "EndElementId": "4:3a2b09d5-2923-45d7-8453-8b1c698427b0:15", "Type": "HAS_AGREEMENT_WITH", "Props": { "create_time": "2025-06-09T15:12:46.374Z", "id": "48BJHS2CTFKcVpD4cUF8IA", "update_time": "2025-06-09T15:12:46.374Z" } } } }, { "nodes": { "subject.external_id": "application_external_id" }, "relationships": { "r1": { "Id": 1152932499723124700, "ElementId": "5:3a2b09d5-2923-45d7-8453-8b1c698427b0:1152932499723124736", "StartId": 0, "StartElementId": "4:3a2b09d5-2923-45d7-8453-8b1c698427b0:0", "EndId": 15, "EndElementId": "4:3a2b09d5-2923-45d7-8453-8b1c698427b0:15", "Type": "HAS_AGREEMENT_WITH", "Props": { "create_time": "2025-06-09T15:12:46.374Z", "id": "48BJHS2CTFKcVpD4cUF8IA", "update_time": "2025-06-09T15:12:46.374Z" } } } }, { "nodes": { "subject.external_id": "application_external_id" }, "relationships": { "r1": { "Id": 1152932499723124700, "ElementId": "5:3a2b09d5-2923-45d7-8453-8b1c698427b0:1152932499723124736", "StartId": 0, "StartElementId": "4:3a2b09d5-2923-45d7-8453-8b1c698427b0:0", "EndId": 15, "EndElementId": "4:3a2b09d5-2923-45d7-8453-8b1c698427b0:15", "Type": "HAS_AGREEMENT_WITH", "Props": { "create_time": "2025-06-09T15:12:46.374Z", "id": "48BJHS2CTFKcVpD4cUF8IA", "update_time": "2025-06-09T15:12:46.374Z" } } } } ] } ``` ### Step 5a Policy JSON with allowed_upserts section defining: (1) Can CREATE Contract nodes, (2) Can CREATE COVERS relationships between Contract and Vehicle, (3) Can CREATE ACCEPTED relationships between Person and Contract. **policy.json** ```json { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "_Application" }, "condition": { "cypher": "MATCH (subject:_Application)-[r1:HAS_AGREEMENT_WITH]->(company:Company)-[r2:OWNS]->(vehicle:Vehicle) MATCH (person:Person)", "filter": [ { "operator": "AND", "operands": [ { "attribute": "subject.external_id", "operator": "=", "value": "$_appId" }, { "attribute": "vehicle.external_id", "operator": "=", "value": "$vehicleID" }, { "attribute": "person.external_id", "operator": "=", "value": "$personID" } ] } ] }, "allowed_upserts": { "nodes": { "node_types": [ "Contract" ] }, "relationships": { "relationship_types": [ { "type": "COVERS", "source_node_label": "Contract", "target_node_label": "Vehicle" }, { "type": "ACCEPTED", "source_node_label": "Person", "target_node_label": "Contract" } ] } }, "allowed_reads": { "nodes": [ "vehicle.property.*", "person.property.*" ], "relationships": [] } } ``` ### Step 5b POST request to create the contract write policy. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"meta\":{\"policy_version\":\"1.0-ciq\"},\"subject\":{\"type\":\"_Application\"},\"condition\":{\"cypher\":\"MATCH (subject:_Application)-[r1:HAS_AGREEMENT_WITH]->(company:Company)-[r2:OWNS]->(vehicle:Vehicle) MATCH (person:Person)\",\"filter\":[{\"operator\":\"AND\",\"operands\":[{\"attribute\":\"subject.external_id\",\"operator\":\"=\",\"value\":\"$_appId\"},{\"attribute\":\"vehicle.external_id\",\"operator\":\"=\",\"value\":\"$vehicleID\"},{\"attribute\":\"person.external_id\",\"operator\":\"=\",\"value\":\"$personID\"}]}]},\"allowed_upserts\":{\"nodes\":{\"node_types\":[\"Contract\"]},\"relationships\":{\"relationship_types\":[{\"type\":\"COVERS\",\"source_node_label\":\"Contract\",\"target_node_label\":\"Vehicle\"},{\"type\":\"ACCEPTED\",\"source_node_label\":\"Person\",\"target_node_label\":\"Contract\"}]}},\"allowed_reads\":{\"nodes\":[\"vehicle.property.*\",\"person.property.*\"],\"relationships\":[]}}", "status": "ACTIVE", "tags": [] } ``` ### Step 5c GET request to verify the policy was created. **GET https://eu.api.indykite.com/configs/v1/authorization-policies/{policy_id}** ```json { "id": "your_policy_configuration_gid" } ``` ### Step 6a Parameterized write query JSON. Uses $contractId and $contractNumber variables for Contract properties. At execution time, actual values replace these placeholders. **knowledge_query.json** ```json { "nodes": [ "contract.external_id", "contract.property.number" ], "relationships": [], "upsert_nodes": [ { "name": "contract", "type": "Contract", "external_id": "$contract_external_id", "properties": [ { "type": "number", "value": "$contractNumber" }, { "type": "status", "value": "Active" } ] } ], "upsert_relationships": [ { "name": "r3", "source": "contract", "target": "vehicle", "type": "COVERS" }, { "name": "r4", "source": "person", "target": "contract", "type": "ACCEPTED" } ] } ``` ### Step 6b POST request to create the contract write query. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "description of knowledge query", "display_name": "knowledge query name", "name": "knowledge-query-name", "policy_id": "your_policy_gid", "query": "{\"nodes\":[\"contract.external_id\",\"contract.property.number\"],\"relationships\":[],\"upsert_nodes\":[{\"name\":\"contract\",\"type\":\"Contract\",\"external_id\":\"$contract_external_id\",\"properties\":[{\"type\":\"number\",\"value\":\"$contractNumber\"},{\"type\":\"status\",\"value\":\"Active\"}]}],\"upsert_relationships\":[{\"name\":\"r3\",\"source\":\"contract\",\"target\":\"vehicle\",\"type\":\"COVERS\"},{\"name\":\"r4\",\"source\":\"person\",\"target\":\"contract\",\"type\":\"ACCEPTED\"}]}", "status": "ACTIVE" } ``` ### Step 6c GET request to verify the query was created. **GET https://eu.api.indykite.com/configs/v1/knowledge-queries/{query_id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 7a Execute the write query with parameter values. Creates: (1) Contract node with specified ID and number, (2) COVERS relationship to Vehicle, (3) ACCEPTED relationship from Person. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "knowledge_query_gid", "input_params": { "vehicleID": "car2", "personID": "ryan", "contract_external_id": "ct853", "contractNumber": "rbjh853" } } ``` ### Step 7b Response confirming the Contract node and relationships were created. **response.json** ```json { "data": [ { "nodes": { "contract.external_id": "ct853", "contract.property.number": "rbjh853" } } ] } ``` ### Step 8a DELETE request to remove the queries. **DELETE https://eu.api.indykite.com/configs/v1/knowledge-queries/{query_id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 8b DELETE request to remove the policies. Note: The created Contract node and relationships remain in the graph. **DELETE https://eu.api.indykite.com/configs/v1/authorization-policies/{policy_id}** ```json { "id": "your_policy_configuration_gid" } ``` ## Common Errors ### 401: UNAUTHENTICATED **Solution:** Check credentials: ServiceAccount Bearer token for config APIs, X-IK-ClientKey for execution ### 404: NOT_FOUND **Solution:** Verify the query/policy ID exists and belongs to your project --- Source: https://developer.indykite.com/resources/ciq-4 --- # ContX IQ: Upsert Relationships Between Existing Nodes > Create or update (upsert) relationships between existing nodes in the graph. This example demonstrates using the allowed_upserts.relationships policy directive to authorize ACCEPTED relationships between Person and Contract nodes. **Category:** ContX IQ **API:** ContX IQ **Tags:** ContX IQ Policy, ContX IQ Query, ContX IQ Execution, Upsert, Relationship Creation, Write Operations **Last Updated:** 2026-03-20 **OpenAPI Endpoints:** /capture/v1/nodes, /capture/v1/relationships, /configs/v1/authorization-policies, /configs/v1/knowledge-queries, /contx-iq/v1/execute **Related Guides:** /guides/guide-contx-iq, /guides/guide-sandbox ## Summary This example demonstrates relationship upsert operations: Key concept: "Upsert" means "insert or update" - if the relationship exists, it's updated; if not, it's created. Policy configuration: - Uses allowed_upserts.relationships to specify which relationship types can be created/updated - In this example: ACCEPTED relationships between Person and Contract nodes Workflow: 1. Link Application to Company (prerequisite for authorization) 2. Create policy with allowed_upserts.relationships directive 3. Create and execute query that upserts Person -[ACCEPTED]-> Contract relationships Difference from ciq-4: This example focuses on upserting relationships only (no new nodes created). ## Use Case Scenario: Your application needs to record when a person accepts a contract. Operation: Create an ACCEPTED relationship from Person to Contract. Upsert behavior: - If Person -[ACCEPTED]-> Contract doesn't exist: Create new relationship - If it already exists: Update relationship properties (if any) Authorization check: The ContX IQ engine verifies that: 1. The requesting Application is authorized (via $_appId filter) 2. The relationship type (ACCEPTED) is in the allowed_upserts.relationships list 3. The source node (Person) and target node (Contract) match the policy Graph change: Before: Person(Ryan) and Contract(Contract1) exist without connection After: Person(Ryan) -[ACCEPTED]-> Contract(Contract1) ## Requirements Prerequisites: - ServiceAccount credentials: For creating policies and queries (Bearer token) - AppAgent credentials: For data ingestion and query execution (X-IK-ClientKey) Required API access: - POST /capture/v1/nodes/ and /capture/v1/relationships/ (initial data) - POST /configs/v1/authorization-policies (create policies) - POST /configs/v1/knowledge-queries (create queries) - POST /contx-iq/v1/execute (run queries) ## Steps Step 1: Ingest Initial Graph Data - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: POST nodes (Person, Contract, Vehicle, Company) and initial relationships - Result: Graph ready for relationship upsert Step 2: Create Application Linking Policy - Authentication: ServiceAccount credential (Bearer token) - Action: POST policy allowing _Application to create HAS_AGREEMENT_WITH relationships - Result: Policy ID returned Step 3: Create Application Linking Query - Authentication: ServiceAccount credential (Bearer token) - Action: POST write query for Application -> Company linking - Result: Query ID returned Step 4: Execute Application Linking - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: Execute the linking query - Result: _Application connected to Company in graph Step 5: Create Relationship Upsert Policy - Authentication: ServiceAccount credential (Bearer token) - Action: POST policy with allowed_upserts.relationships containing: - source: Person - target: Contract - type: ACCEPTED - Result: Policy ID returned Step 6: Create Relationship Upsert Query - Authentication: ServiceAccount credential (Bearer token) - Action: POST query that upserts ACCEPTED relationships - Result: Query ID returned Step 7: Execute Relationship Upsert - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: Execute the upsert query - Result: Person -[ACCEPTED]-> Contract relationship created/updated Step 8: Cleanup - Action: DELETE queries and policies ## Code Examples ### Step 1a Capture nodes: Person, Contract, Vehicle, Company nodes that will be connected by the upserted relationship. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "alice", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "alice@email.com" }, { "type": "given_name", "value": "Alice" }, { "type": "last_name", "value": "Smith" } ] }, { "external_id": "ryan", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "ryan@yahoo.co.uk" }, { "type": "given_name", "value": "ryan" }, { "type": "last_name", "value": "mushu" } ] }, { "external_id": "tilda", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "tilda@yahoo.co.uk" }, { "type": "given_name", "value": "tilda" }, { "type": "last_name", "value": "mushu" } ] }, { "external_id": "cb123", "type": "PaymentMethod", "properties": [ { "type": "payment_name", "value": "Credit Card" } ] }, { "external_id": "kl123", "type": "PaymentMethod", "properties": [ { "type": "payment_name", "value": "Klarna" } ] }, { "external_id": "ct123", "type": "Contract", "properties": [ { "type": "category", "value": "Insurance" }, { "type": "status", "value": "Active" }, { "type": "number", "value": "hfgrten123", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "ct234", "type": "Contract", "properties": [ { "type": "category", "value": "Insurance" }, { "type": "status", "value": "Active" }, { "type": "number", "value": "hfgrten234", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "ct985", "type": "Contract", "properties": [ { "type": "category", "value": "Insurance" }, { "type": "status", "value": "Active" }, { "type": "number", "value": "hfgrten985", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "car1", "type": "Vehicle", "properties": [ { "type": "category", "value": "Car" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "rtfhcnvjt471" } ] }, { "external_id": "car2", "type": "Vehicle", "properties": [ { "type": "category", "value": "Car" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "kdcbfrt178" } ] }, { "external_id": "truck1", "type": "Vehicle", "properties": [ { "type": "category", "value": "Truck" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "sncnrkcldp" } ] }, { "external_id": "license1", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "AX123456", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "license2", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "OL123456", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "license3", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "VN123456", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "company1", "type": "Company", "properties": [ { "type": "name", "value": "Company1" }, { "type": "registration", "value": "256314523" } ] }, { "external_id": "company2", "type": "Company", "properties": [ { "type": "name", "value": "Company2" }, { "type": "registration", "value": "942365123" } ] }, { "external_id": "application1", "type": "Application", "properties": [ { "type": "name", "value": "Application" } ] }, { "external_id": "application2", "type": "Application", "properties": [ { "type": "name", "value": "Application2" } ] } ] } ``` ### Step 1b Capture initial relationships (not including ACCEPTED - that will be upserted). **POST https://eu.api.indykite.com/capture/v1/relationships/** ```json { "relationships": [ { "source": { "external_id": "ryan", "type": "Person" }, "target": { "external_id": "cb123", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "tilda", "type": "Person" }, "target": { "external_id": "kl123", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "cb123", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "ryan", "type": "Person" }, "target": { "external_id": "ct123", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "tilda", "type": "Person" }, "target": { "external_id": "ct234", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "ct985", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "ct123", "type": "Contract" }, "target": { "external_id": "car1", "type": "Vehicle" }, "type": "COVERS" }, { "source": { "external_id": "ct985", "type": "Contract" }, "target": { "external_id": "car1", "type": "Vehicle" }, "type": "COVERS" }, { "source": { "external_id": "ct234", "type": "Contract" }, "target": { "external_id": "truck1", "type": "Vehicle" }, "type": "COVERS" }, { "source": { "external_id": "car1", "type": "Vehicle" }, "target": { "external_id": "license1", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "truck1", "type": "Vehicle" }, "target": { "external_id": "license2", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "car2", "type": "Vehicle" }, "target": { "external_id": "license3", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "company1", "type": "Company" }, "target": { "external_id": "car1", "type": "Vehicle" }, "type": "OWNS" }, { "source": { "external_id": "company1", "type": "Company" }, "target": { "external_id": "car2", "type": "Vehicle" }, "type": "OWNS" }, { "source": { "external_id": "company1", "type": "Company" }, "target": { "external_id": "truck1", "type": "Vehicle" }, "type": "OWNS" }, { "source": { "external_id": "application1", "type": "Application" }, "target": { "external_id": "company1", "type": "Company" }, "type": "HAS_AGREEMENT_WITH" }, { "source": { "external_id": "application2", "type": "Application" }, "target": { "external_id": "company1", "type": "Company" }, "type": "HAS_AGREEMENT_WITH" } ] } ``` ### Step 2a Policy JSON allowing _Application to create HAS_AGREEMENT_WITH relationships (prerequisite for authorization). **policy.json** ```json { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "_Application" }, "condition": { "cypher": "MATCH (subject:_Application) MATCH (company:Company)-[r2:OWNS]->(vehicle:Vehicle)", "filter": [ { "operator": "AND", "operands": [ { "attribute": "subject.external_id", "operator": "=", "value": "$_appId" }, { "attribute": "company.external_id", "operator": "=", "value": "$companyID" } ] } ] }, "allowed_upserts": { "relationships": { "relationship_types": [ { "type": "HAS_AGREEMENT_WITH", "source_node_label": "_Application", "target_node_label": "Company" } ] } }, "allowed_reads": { "nodes": [ "company.*", "subject.*" ] } } ``` ### Step 2b POST request to create the application linking policy. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"meta\":{\"policy_version\":\"1.0-ciq\"},\"subject\":{\"type\":\"_Application\"},\"condition\":{\"cypher\":\"MATCH (subject:_Application) MATCH (company:Company)-[r2:OWNS]->(vehicle:Vehicle)\",\"filter\":[{\"operator\":\"AND\",\"operands\":[{\"attribute\":\"subject.external_id\",\"operator\":\"=\",\"value\":\"$_appId\"},{\"attribute\":\"company.external_id\",\"operator\":\"=\",\"value\":\"$companyID\"}]}]},\"allowed_upserts\":{\"relationships\":{\"relationship_types\":[{\"type\":\"HAS_AGREEMENT_WITH\",\"source_node_label\":\"_Application\",\"target_node_label\":\"Company\"}]}},\"allowed_reads\":{\"nodes\":[\"company.*\",\"subject.*\"]}}", "status": "ACTIVE", "tags": [] } ``` ### Step 2c GET request to verify the policy was created. **GET https://eu.api.indykite.com/configs/v1/authorization-policies/{policy_id}** ```json { "id": "your_policy_configuration_gid" } ``` ### Step 3a Write query JSON for Application -> Company linking. **knowledge_query.json** ```json { "nodes": [ "subject.external_id" ], "relationships": [ "r1" ], "upsert_relationships": [ { "name": "r1", "source": "subject", "target": "company", "type": "HAS_AGREEMENT_WITH" } ] } ``` ### Step 3b POST request to create the linking query. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "description of knowledge query", "display_name": "knowledge query name", "name": "knowledge-query-name", "policy_id": "your_policy_gid", "query": "{\"nodes\":[\"subject.external_id\"],\"relationships\":[\"r1\"],\"upsert_relationships\":[{\"name\":\"r1\",\"source\":\"subject\",\"target\":\"company\",\"type\":\"HAS_AGREEMENT_WITH\"}]}", "status": "ACTIVE" } ``` ### Step 3c GET request to verify the query was created. **GET https://eu.api.indykite.com/configs/v1/knowledge-queries/{query_id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 4a Execute the linking query to connect Application to Company. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "ciq_query_gid", "input_params": { "companyID": "company1" } } ``` ### Step 4b Response confirming the Application -> Company link was created. **response.json** ```json { "data": [ { "nodes": { "subject.external_id": "application_external_id" }, "relationships": { "r1": { "Id": 1152932499723124700, "ElementId": "5:3a2b09d5-2923-45d7-8453-8b1c698427b0:1152932499723124736", "StartId": 0, "StartElementId": "4:3a2b09d5-2923-45d7-8453-8b1c698427b0:0", "EndId": 15, "EndElementId": "4:3a2b09d5-2923-45d7-8453-8b1c698427b0:15", "Type": "HAS_AGREEMENT_WITH", "Props": { "create_time": "2025-06-09T15:12:46.374Z", "id": "48BJHS2CTFKcVpD4cUF8IA", "update_time": "2025-06-09T15:12:46.374Z" } } } }, { "nodes": { "subject.external_id": "application_external_id" }, "relationships": { "r1": { "Id": 1152932499723124700, "ElementId": "5:3a2b09d5-2923-45d7-8453-8b1c698427b0:1152932499723124736", "StartId": 0, "StartElementId": "4:3a2b09d5-2923-45d7-8453-8b1c698427b0:0", "EndId": 15, "EndElementId": "4:3a2b09d5-2923-45d7-8453-8b1c698427b0:15", "Type": "HAS_AGREEMENT_WITH", "Props": { "create_time": "2025-06-09T15:12:46.374Z", "id": "48BJHS2CTFKcVpD4cUF8IA", "update_time": "2025-06-09T15:12:46.374Z" } } } }, { "nodes": { "subject.external_id": "application_external_id" }, "relationships": { "r1": { "Id": 1152932499723124700, "ElementId": "5:3a2b09d5-2923-45d7-8453-8b1c698427b0:1152932499723124736", "StartId": 0, "StartElementId": "4:3a2b09d5-2923-45d7-8453-8b1c698427b0:0", "EndId": 15, "EndElementId": "4:3a2b09d5-2923-45d7-8453-8b1c698427b0:15", "Type": "HAS_AGREEMENT_WITH", "Props": { "create_time": "2025-06-09T15:12:46.374Z", "id": "48BJHS2CTFKcVpD4cUF8IA", "update_time": "2025-06-09T15:12:46.374Z" } } } } ] } ``` ### Step 5a Policy JSON with allowed_upserts.relationships directive. Specifies that ACCEPTED relationships can be created between Person (source) and Contract (target) nodes. **policy.json** ```json { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "_Application" }, "condition": { "cypher": "MATCH (subject:_Application)-[r1:HAS_AGREEMENT_WITH]->(company:Company)-[r2:OWNS]->(vehicle:Vehicle)<-[r3:COVERS]-(contract:Contract) MATCH (person:Person)", "filter": [ { "operator": "AND", "operands": [ { "attribute": "subject.external_id", "operator": "=", "value": "$_appId" }, { "attribute": "contract.external_id", "operator": "=", "value": "$contractID" }, { "attribute": "person.external_id", "operator": "=", "value": "$personID" } ] } ] }, "allowed_upserts": { "relationships": { "relationship_types": [ { "type": "ACCEPTED", "source_node_label": "Person", "target_node_label": "Contract" } ] } }, "allowed_reads": { "nodes": [ "person.*" ] } } ``` ### Step 5b POST request to create the relationship upsert policy. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"meta\":{\"policy_version\":\"1.0-ciq\"},\"subject\":{\"type\":\"_Application\"},\"condition\":{\"cypher\":\"MATCH (subject:_Application)-[r1:HAS_AGREEMENT_WITH]->(company:Company)-[r2:OWNS]->(vehicle:Vehicle)<-[r3:COVERS]-(contract:Contract) MATCH (person:Person)\",\"filter\":[{\"operator\":\"AND\",\"operands\":[{\"attribute\":\"subject.external_id\",\"operator\":\"=\",\"value\":\"$_appId\"},{\"attribute\":\"contract.external_id\",\"operator\":\"=\",\"value\":\"$contractID\"},{\"attribute\":\"person.external_id\",\"operator\":\"=\",\"value\":\"$personID\"}]}]},\"allowed_upserts\":{\"relationships\":{\"relationship_types\":[{\"type\":\"ACCEPTED\",\"source_node_label\":\"Person\",\"target_node_label\":\"Contract\"}]}},\"allowed_reads\":{\"nodes\":[\"person.*\"]}}", "status": "ACTIVE", "tags": [] } ``` ### Step 5c GET request to verify the policy was created. **GET https://eu.api.indykite.com/configs/v1/authorization-policies/{policy_id}** ```json { "id": "your_policy_configuration_gid" } ``` ### Step 6a Query JSON that upserts ACCEPTED relationships. The query matches Person and Contract nodes, then creates/updates the ACCEPTED relationship between them. **knowledge_query.json** ```json { "nodes": [ "person.external_id" ], "relationships": [ "r4" ], "upsert_relationships": [ { "name": "r4", "source": "person", "target": "contract", "type": "ACCEPTED" } ] } ``` ### Step 6b POST request to create the upsert query. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "description of knowledge query", "display_name": "knowledge query name", "name": "knowledge-query-name", "policy_id": "your_policy_gid", "query": "{\"nodes\":[\"person.external_id\"],\"relationships\":[\"r4\"],\"upsert_relationships\":[{\"name\":\"r4\",\"source\":\"person\",\"target\":\"contract\",\"type\":\"ACCEPTED\"}]}", "status": "ACTIVE" } ``` ### Step 6c GET request to verify the query was created. **GET https://eu.api.indykite.com/configs/v1/knowledge-queries/{query_id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 7a Execute the upsert query. Creates Person -[ACCEPTED]-> Contract relationship. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "knowledge_query_gid", "input_params": { "contractID": "ct123", "personID": "tilda" } } ``` ### Step 7b Response confirming the ACCEPTED relationship was upserted. **response.json** ```json { "data": [ { "nodes": { "person.external_id": "tilda" }, "relationships": { "r4": { "Id": 1155177702467043300, "ElementId": "5:3a2b09d5-2923-45d7-8453-8b1c698427b0:1155177702467043332", "StartId": 4, "StartElementId": "4:3a2b09d5-2923-45d7-8453-8b1c698427b0:4", "EndId": 8, "EndElementId": "4:3a2b09d5-2923-45d7-8453-8b1c698427b0:8", "Type": "ACCEPTED", "Props": { "create_time": "2025-06-09T10:15:23.494Z", "id": "ioSB2XEIRQCjd5hdxkf48w", "update_time": "2025-06-09T10:15:23.494Z" } } } } ] } ``` ### Step 8a DELETE request to remove the queries. **DELETE https://eu.api.indykite.com/configs/v1/knowledge-queries/{query_id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 8b DELETE request to remove the policies. Note: The upserted relationships remain in the graph. **DELETE https://eu.api.indykite.com/configs/v1/authorization-policies/{policy_id}** ```json { "id": "your_policy_configuration_gid" } ``` ## Common Errors ### 401: UNAUTHENTICATED **Solution:** Check credentials: ServiceAccount Bearer token for config APIs, X-IK-ClientKey for execution ### 404: NOT_FOUND **Solution:** Verify the query/policy ID exists and belongs to your project --- Source: https://developer.indykite.com/resources/ciq-5 --- # ContX IQ: Delete Nodes and Their Relationships via Authorized Query > Demonstrates authorized node deletion in the IndyKite Knowledge Graph. When a node is deleted, all its relationships are automatically removed. This example shows deleting Contract nodes for a specific Person. **Category:** ContX IQ **API:** ContX IQ **Tags:** ContX IQ Policy, ContX IQ Query, ContX IQ Execution, Delete Operations, Node Deletion, Cascade Delete **Last Updated:** 2026-03-20 **OpenAPI Endpoints:** /capture/v1/nodes, /capture/v1/relationships, /configs/v1/authorization-policies, /configs/v1/knowledge-queries, /contx-iq/v1/execute **Related Guides:** /guides/guide-contx-iq, /guides/guide-sandbox ## Summary This example demonstrates DELETE operations in ContX IQ: Key concepts: - Deleting a node automatically removes all relationships connected to that node - The policy uses allowed_deletes.nodes to specify which node types can be deleted - Authorization ensures only permitted applications can perform deletions Workflow: 1. Capture data including Contract nodes 2. Link Application to Company (for authorization) 3. Create delete policy (allowed_deletes.nodes: Contract) 4. Execute delete query to remove Contract nodes Important: Node deletion is cascading - when a Contract is deleted, its COVERS and ACCEPTED relationships are also removed. ## Use Case Scenario: A person wants to cancel all their vehicle rental contracts. Operation: Delete all Contract nodes associated with a specific Person. Graph change: Before: - Person(Ryan) -[ACCEPTED]-> Contract1 -[COVERS]-> Vehicle(Car1) - Person(Ryan) -[ACCEPTED]-> Contract2 -[COVERS]-> Vehicle(Car2) After deletion: - Person(Ryan) (no contracts) - Vehicle(Car1) (still exists, but no Contract relationship) - Vehicle(Car2) (still exists, but no Contract relationship) Cascade behavior: Deleting Contract1 automatically removes: - The ACCEPTED relationship from Person to Contract1 - The COVERS relationship from Contract1 to Vehicle ## Requirements Prerequisites: - ServiceAccount credentials: For creating policies and queries (Bearer token) - AppAgent credentials: For data ingestion and query execution (X-IK-ClientKey) Required API access: - POST /capture/v1/nodes/ and /capture/v1/relationships/ (initial data) - POST /configs/v1/authorization-policies (create policies) - POST /configs/v1/knowledge-queries (create queries) - POST /contx-iq/v1/execute (run queries) ## Steps Step 1: Ingest Graph Data with Contracts - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: POST nodes and relationships including Contract nodes to be deleted later - Result: Graph with Person -> Contract -> Vehicle structure Step 2: Create Application Linking Policy - Authentication: ServiceAccount credential (Bearer token) - Action: POST policy allowing Application to create HAS_AGREEMENT_WITH - Key detail: $_appId filter auto-resolves to calling Application's ID - Result: Policy ID returned Step 3: Create Application Linking Query - Authentication: ServiceAccount credential (Bearer token) - Action: POST write query for Application -> Company relationship - Result: Query ID returned Step 4: Execute Application Linking - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: Execute query to connect Application to Company - Result: Application authorized for subsequent operations Step 5: Create Node Delete Policy - Authentication: ServiceAccount credential (Bearer token) - Action: POST policy with allowed_deletes.nodes containing Contract - This authorizes queries to delete Contract-type nodes - Result: Policy ID returned Step 6: Create Delete Query - Authentication: ServiceAccount credential (Bearer token) - Action: POST query that identifies and deletes Contract nodes - Query can use parameters to specify which contracts to delete - Result: Query ID returned Step 7: Execute Deletion - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: Execute the delete query - Result: Contract nodes and their relationships removed from graph Step 8: Cleanup - Action: DELETE queries and policies (the deleted nodes remain gone) ## Code Examples ### Step 1a Capture nodes including Contract nodes that will be deleted later. Also creates Person, Vehicle, Company, and LicenseNumber nodes. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "alice", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "alice@email.com" }, { "type": "given_name", "value": "Alice" }, { "type": "last_name", "value": "Smith" } ] }, { "external_id": "ryan", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "ryan@yahoo.co.uk" }, { "type": "given_name", "value": "ryan" }, { "type": "last_name", "value": "mushu" } ] }, { "external_id": "tilda", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "tilda@yahoo.co.uk" }, { "type": "given_name", "value": "tilda" }, { "type": "last_name", "value": "mushu" } ] }, { "external_id": "cb123", "type": "PaymentMethod", "properties": [ { "type": "payment_name", "value": "Credit Card" } ] }, { "external_id": "kl123", "type": "PaymentMethod", "properties": [ { "type": "payment_name", "value": "Klarna" } ] }, { "external_id": "ct123", "type": "Contract", "properties": [ { "type": "category", "value": "Insurance" }, { "type": "status", "value": "Active" }, { "type": "number", "value": "hfgrten123", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "ct234", "type": "Contract", "properties": [ { "type": "category", "value": "Insurance" }, { "type": "status", "value": "Active" }, { "type": "number", "value": "hfgrten234", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "ct985", "type": "Contract", "properties": [ { "type": "category", "value": "Insurance" }, { "type": "status", "value": "Active" }, { "type": "number", "value": "hfgrten985", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "car1", "type": "Vehicle", "properties": [ { "type": "category", "value": "Car" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "rtfhcnvjt471" } ] }, { "external_id": "car2", "type": "Vehicle", "properties": [ { "type": "category", "value": "Car" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "kdcbfrt178" } ] }, { "external_id": "truck1", "type": "Vehicle", "properties": [ { "type": "category", "value": "Truck" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "sncnrkcldp" } ] }, { "external_id": "license1", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "AX123456", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "license2", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "OL123456", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "license3", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "VN123456", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "company1", "type": "Company", "properties": [ { "type": "name", "value": "Company1" }, { "type": "registration", "value": "256314523" } ] }, { "external_id": "company2", "type": "Company", "properties": [ { "type": "name", "value": "Company2" }, { "type": "registration", "value": "942365123" } ] }, { "external_id": "application1", "type": "Application", "properties": [ { "type": "name", "value": "Application" } ] }, { "external_id": "application2", "type": "Application", "properties": [ { "type": "name", "value": "Application2" } ] } ] } ``` ### Step 1b Capture relationships: Person -[ACCEPTED]-> Contract -[COVERS]-> Vehicle. These relationships will be cascade-deleted when the Contract is deleted. **POST https://eu.api.indykite.com/capture/v1/relationships/** ```json { "relationships": [ { "source": { "external_id": "ryan", "type": "Person" }, "target": { "external_id": "cb123", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "tilda", "type": "Person" }, "target": { "external_id": "kl123", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "cb123", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "ryan", "type": "Person" }, "target": { "external_id": "ct123", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "tilda", "type": "Person" }, "target": { "external_id": "ct234", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "ct985", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "ct123", "type": "Contract" }, "target": { "external_id": "car1", "type": "Vehicle" }, "type": "COVERS" }, { "source": { "external_id": "ct985", "type": "Contract" }, "target": { "external_id": "car1", "type": "Vehicle" }, "type": "COVERS" }, { "source": { "external_id": "ct234", "type": "Contract" }, "target": { "external_id": "truck1", "type": "Vehicle" }, "type": "COVERS" }, { "source": { "external_id": "car1", "type": "Vehicle" }, "target": { "external_id": "license1", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "truck1", "type": "Vehicle" }, "target": { "external_id": "license2", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "car2", "type": "Vehicle" }, "target": { "external_id": "license3", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "company1", "type": "Company" }, "target": { "external_id": "car1", "type": "Vehicle" }, "type": "OWNS" }, { "source": { "external_id": "company1", "type": "Company" }, "target": { "external_id": "car2", "type": "Vehicle" }, "type": "OWNS" }, { "source": { "external_id": "company1", "type": "Company" }, "target": { "external_id": "truck1", "type": "Vehicle" }, "type": "OWNS" }, { "source": { "external_id": "application1", "type": "Application" }, "target": { "external_id": "company1", "type": "Company" }, "type": "HAS_AGREEMENT_WITH" }, { "source": { "external_id": "application2", "type": "Application" }, "target": { "external_id": "company1", "type": "Company" }, "type": "HAS_AGREEMENT_WITH" } ] } ``` ### Step 2a Policy JSON allowing _Application to create HAS_AGREEMENT_WITH relationships. The $_appId filter auto-populates with the calling Application's ID. **policy.json** ```json { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "_Application" }, "condition": { "cypher": "MATCH (subject:_Application) MATCH (company:Company)-[r2:OWNS]->(vehicle:Vehicle)", "filter": [ { "operator": "AND", "operands": [ { "attribute": "subject.external_id", "operator": "=", "value": "$_appId" }, { "attribute": "company.external_id", "operator": "=", "value": "$companyID" } ] } ] }, "allowed_upserts": { "relationships": { "relationship_types": [ { "type": "HAS_AGREEMENT_WITH", "source_node_label": "_Application", "target_node_label": "Company" } ] } }, "allowed_reads": { "nodes": [ "company.*", "subject.*" ] } } ``` ### Step 2b POST request to create the application linking policy. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"meta\":{\"policy_version\":\"1.0-ciq\"},\"subject\":{\"type\":\"_Application\"},\"condition\":{\"cypher\":\"MATCH (subject:_Application) MATCH (company:Company)-[r2:OWNS]->(vehicle:Vehicle)\",\"filter\":[{\"operator\":\"AND\",\"operands\":[{\"attribute\":\"subject.external_id\",\"operator\":\"=\",\"value\":\"$_appId\"},{\"attribute\":\"company.external_id\",\"operator\":\"=\",\"value\":\"$companyID\"}]}]},\"allowed_upserts\":{\"relationships\":{\"relationship_types\":[{\"type\":\"HAS_AGREEMENT_WITH\",\"source_node_label\":\"_Application\",\"target_node_label\":\"Company\"}]}},\"allowed_reads\":{\"nodes\":[\"company.*\",\"subject.*\"]}}", "status": "ACTIVE", "tags": [] } ``` ### Step 2c GET request to verify the policy was created. **GET https://eu.api.indykite.com/configs/v1/authorization-policies/{policy_id}** ```json { "id": "your_policy_configuration_gid" } ``` ### Step 3a Write query JSON for creating Application -> Company relationship. **knowledge_query.json** ```json { "nodes": [ "subject.external_id" ], "relationships": [ "r1" ], "upsert_relationships": [ { "name": "r1", "source": "subject", "target": "company", "type": "HAS_AGREEMENT_WITH" } ] } ``` ### Step 3b POST request to create the linking query. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "description of knowledge query", "display_name": "knowledge query name", "name": "knowledge-query-name", "policy_id": "your_policy_gid", "query": "{\"nodes\":[\"subject.external_id\"],\"relationships\":[\"r1\"],\"upsert_relationships\":[{\"name\":\"r1\",\"source\":\"subject\",\"target\":\"company\",\"type\":\"HAS_AGREEMENT_WITH\"}]}", "status": "ACTIVE" } ``` ### Step 3c GET request to verify the query was created. **GET https://eu.api.indykite.com/configs/v1/knowledge-queries/{query_id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 4a Execute the linking query. Establishes Application's authorization context. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "ciq_query_gid", "input_params": { "companyID": "company1" } } ``` ### Step 4b Response confirming the Application -> Company link was created. **response.json** ```json { "data": [ { "nodes": { "subject.external_id": "application_external_id" }, "relationships": { "r1": { "Id": 1152932499723124700, "ElementId": "5:3a2b09d5-2923-45d7-8453-8b1c698427b0:1152932499723124736", "StartId": 0, "StartElementId": "4:3a2b09d5-2923-45d7-8453-8b1c698427b0:0", "EndId": 15, "EndElementId": "4:3a2b09d5-2923-45d7-8453-8b1c698427b0:15", "Type": "HAS_AGREEMENT_WITH", "Props": { "create_time": "2025-06-09T15:12:46.374Z", "id": "48BJHS2CTFKcVpD4cUF8IA", "update_time": "2025-06-09T15:12:46.374Z" } } } }, { "nodes": { "subject.external_id": "application_external_id" }, "relationships": { "r1": { "Id": 1152932499723124700, "ElementId": "5:3a2b09d5-2923-45d7-8453-8b1c698427b0:1152932499723124736", "StartId": 0, "StartElementId": "4:3a2b09d5-2923-45d7-8453-8b1c698427b0:0", "EndId": 15, "EndElementId": "4:3a2b09d5-2923-45d7-8453-8b1c698427b0:15", "Type": "HAS_AGREEMENT_WITH", "Props": { "create_time": "2025-06-09T15:12:46.374Z", "id": "48BJHS2CTFKcVpD4cUF8IA", "update_time": "2025-06-09T15:12:46.374Z" } } } }, { "nodes": { "subject.external_id": "application_external_id" }, "relationships": { "r1": { "Id": 1152932499723124700, "ElementId": "5:3a2b09d5-2923-45d7-8453-8b1c698427b0:1152932499723124736", "StartId": 0, "StartElementId": "4:3a2b09d5-2923-45d7-8453-8b1c698427b0:0", "EndId": 15, "EndElementId": "4:3a2b09d5-2923-45d7-8453-8b1c698427b0:15", "Type": "HAS_AGREEMENT_WITH", "Props": { "create_time": "2025-06-09T15:12:46.374Z", "id": "48BJHS2CTFKcVpD4cUF8IA", "update_time": "2025-06-09T15:12:46.374Z" } } } } ] } ``` ### Step 5a Policy JSON with allowed_deletes.nodes directive. Specifies that Contract nodes can be deleted by authorized queries. **policy.json** ```json { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "_Application" }, "condition": { "cypher": "MATCH (subject : _Application)-[r1:HAS_AGREEMENT_WITH]->(company:Company)-[r2:OWNS]->(vehicle:Vehicle)<-[r3:COVERS]-(contract:Contract)<-[r4:ACCEPTED]-(person:Person)", "filter": [ { "operator": "AND", "operands": [ { "attribute": "subject.external_id", "operator": "=", "value": "$_appId" }, { "attribute": "person.external_id", "operator": "=", "value": "$personID" } ] } ] }, "allowed_deletes": { "nodes": [ "contract" ] }, "allowed_reads": { "nodes": [ "person.*" ] } } ``` ### Step 5b POST request to create the node delete policy. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"meta\":{\"policy_version\":\"1.0-ciq\"},\"subject\":{\"type\":\"_Application\"},\"condition\":{\"cypher\":\"MATCH (subject : _Application)-[r1:HAS_AGREEMENT_WITH]->(company:Company)-[r2:OWNS]->(vehicle:Vehicle)<-[r3:COVERS]-(contract:Contract)<-[r4:ACCEPTED]-(person:Person)\",\"filter\":[{\"operator\":\"AND\",\"operands\":[{\"attribute\":\"subject.external_id\",\"operator\":\"=\",\"value\":\"$_appId\"},{\"attribute\":\"person.external_id\",\"operator\":\"=\",\"value\":\"$personID\"}]}]},\"allowed_deletes\":{\"nodes\":[\"contract\"]},\"allowed_reads\":{\"nodes\":[\"person.*\"]}}", "status": "ACTIVE", "tags": [] } ``` ### Step 5c GET request to verify the policy was created. **GET https://eu.api.indykite.com/configs/v1/authorization-policies/{policy_id}** ```json { "id": "your_policy_configuration_gid" } ``` ### Step 6a Delete query JSON. Identifies Contract nodes to delete (e.g., by Person email or Contract ID). All relationships to/from deleted nodes are automatically removed. **knowledge_query.json** ```json { "nodes": [ "person.external_id" ], "delete_nodes": [ "contract" ] } ``` ### Step 6b POST request to create the delete query. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "description of knowledge query", "display_name": "knowledge query name", "name": "knowledge-query-name", "policy_id": "your_policy_gid", "query": "{\"nodes\":[\"person.external_id\"],\"delete_nodes\":[\"contract\"]}", "status": "ACTIVE" } ``` ### Step 6c GET request to verify the query was created. **GET https://eu.api.indykite.com/configs/v1/knowledge-queries/{query_id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 7a Execute the delete query. Removes Contract nodes and their ACCEPTED/COVERS relationships from the graph. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "knowledge_query_gid", "input_params": { "personID": "tilda" } } ``` ### Step 7b Response confirming the Contract nodes were deleted. The Person and Vehicle nodes remain, but are no longer connected via contracts. **response.json** ```json { "data": [ { "nodes": { "person.external_id": "tilda" } }, { "nodes": { "person.external_id": "tilda" } }, { "nodes": { "person.external_id": "tilda" } } ] } ``` ### Step 8a DELETE request to remove the queries. **DELETE https://eu.api.indykite.com/configs/v1/knowledge-queries/{query_id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 8b DELETE request to remove the policies. **DELETE https://eu.api.indykite.com/configs/v1/authorization-policies/{policy_id}** ```json { "id": "your_policy_configuration_gid" } ``` ## Common Errors ### 401: UNAUTHENTICATED **Solution:** Check credentials: ServiceAccount Bearer token for config APIs, X-IK-ClientKey for execution ### 404: NOT_FOUND **Solution:** Verify the query/policy ID exists and belongs to your project --- Source: https://developer.indykite.com/resources/ciq-6 --- # ContX IQ: Loyalty Program - Retrieve Payment Method from License Plate > Real-world loyalty program scenario: Given a vehicle's license plate number, retrieve the credit card associated with an authorized user. Demonstrates multi-condition authorization with consent validation and loyalty plan membership checks. **Category:** ContX IQ **API:** ContX IQ **Tags:** ContX IQ Policy, ContX IQ Query, ContX IQ Execution, Loyalty Program, Consent Validation, Complex Authorization **Last Updated:** 2026-03-20 **OpenAPI Endpoints:** /capture/v1/nodes, /capture/v1/relationships, /configs/v1/authorization-policies, /configs/v1/knowledge-queries, /contx-iq/v1/execute **Related Guides:** /guides/guide-contx-iq, /guides/guide-sandbox ## Summary This example demonstrates a complex real-world authorization scenario for loyalty programs: Input: License plate number Output: Credit card external_id (for authorized requests only) Authorization conditions checked: 1. The vehicle's license plate must be registered 2. All users of the vehicle must be on the same loyalty plan 3. The application must have valid consent (not expired) from the user This showcases ContX IQ's ability to enforce business rules through graph relationships and time-based conditions. ## Use Case Scenario: A gas station loyalty app wants to auto-charge a registered credit card when a vehicle arrives. Business rules enforced by the policy: 1. License plate registration: The car owner must have registered the license plate in the system 2. Loyalty plan consistency: All users who can drive the car must be on the same loyalty plan (prevents conflicts) 3. Consent validation: The app must have unexpired consent from the car owner to access payment info Graph structure: Vehicle -[HAS]-> LicenseNumber Person -[OWNS]-> Vehicle Person -[MEMBER_OF]-> LoyaltyPlan Person -[GAVE_CONSENT]-> Consent -[TO_APP]-> Application Person -[HAS]-> CreditCard Query flow: 1. Find Vehicle by license plate 2. Find Person who owns the Vehicle 3. Verify all Vehicle drivers are on same LoyaltyPlan 4. Check Person's Consent to the requesting App (verify not expired) 5. If all conditions pass, return CreditCard.external_id ## Requirements Prerequisites: - ServiceAccount credentials: For creating policies and queries (Bearer token) - AppAgent credentials: For data ingestion and query execution (X-IK-ClientKey) Required API access: - POST /capture/v1/nodes/ and /capture/v1/relationships/ (loyalty data) - POST /configs/v1/authorization-policies (create policy) - POST /configs/v1/knowledge-queries (create query) - POST /contx-iq/v1/execute (run query) ## Steps Step 1: Ingest Loyalty Program Graph Data - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: POST nodes: Person, Vehicle, LicenseNumber, LoyaltyPlan, Consent, CreditCard, Application - Action: POST relationships: OWNS, HAS, MEMBER_OF, GAVE_CONSENT, TO_APP - Result: Complete loyalty program graph ready for queries Step 2: Create Loyalty Authorization Policy - Authentication: ServiceAccount credential (Bearer token) - Action: POST policy with conditions: - Match vehicle by license plate input - Traverse to owner Person - Verify loyalty plan consistency across all drivers - Validate consent exists and is not expired - Allow READ on CreditCard.external_id if conditions pass - Result: Policy ID returned Step 3: Create Loyalty Query - Authentication: ServiceAccount credential (Bearer token) - Action: POST query that takes license plate as input and returns credit card ID - Input parameter: $licensePlate (string) - Result: Query ID returned Step 4: Execute Loyalty Query - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: POST to /contx-iq/v1/execute with license plate value - Result: Credit card external_id (if authorized) or empty result (if not authorized) Step 5: Cleanup - Action: DELETE query and policy configurations ## Code Examples ### Step 1a Capture loyalty program nodes: Person (car owner), Vehicle, LicenseNumber, LoyaltyPlan, Consent (with expiration date), CreditCard, and Application. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "alice", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "alice@email.com" }, { "type": "given_name", "value": "Alice" }, { "type": "last_name", "value": "Smith" } ] }, { "external_id": "ole", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "ole@yahoo.co.uk" }, { "type": "given_name", "value": "ole" }, { "type": "last_name", "value": "einar" } ] }, { "external_id": "cb2563", "type": "PaymentMethod", "properties": [ { "type": "payment_name", "value": "Credit Card Parking" }, { "type": "preference", "value": "Pay as you go" } ] }, { "external_id": "carOle", "type": "Vehicle", "properties": [ { "type": "category", "value": "Car" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "pcfjnm78" } ] }, { "external_id": "licenseOle", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "AL98745", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "loyalty1", "type": "Loyalty", "properties": [ { "type": "name", "value": "Parking Loyalty Plan" } ] }, { "external_id": "consent1", "type": "ConsentPayment", "properties": [ { "type": "name", "value": "Consent Parking" } ] }, { "external_id": "companyParking", "type": "Company", "properties": [ { "type": "name", "value": "City Parking Inc" } ] }, { "external_id": "applicationParking", "type": "Application", "properties": [ { "type": "name", "value": "City Mall Parking" } ] } ] } ``` ### Step 1b Capture loyalty relationships: Person -[OWNS]-> Vehicle, Vehicle -[HAS]-> LicenseNumber, Person -[MEMBER_OF]-> LoyaltyPlan, Person -[GAVE_CONSENT]-> Consent -[TO_APP]-> Application, Person -[HAS]-> CreditCard. **POST https://eu.api.indykite.com/capture/v1/relationships/** ```json { "relationships": [ { "source": { "external_id": "ole", "type": "Person" }, "target": { "external_id": "cb2563", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "ole", "type": "Person" }, "target": { "external_id": "carOle", "type": "Car" }, "type": "OWNS" }, { "source": { "external_id": "ole", "type": "Person" }, "target": { "external_id": "loyalty1", "type": "Loyalty" }, "type": "IS_MEMBER" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "loyalty1", "type": "Loyalty" }, "type": "IS_MEMBER" }, { "source": { "external_id": "ole", "type": "Person" }, "target": { "external_id": "consent1", "type": "ConsentPayment" }, "type": "GRANTED" }, { "source": { "external_id": "carOle", "type": "Car" }, "target": { "external_id": "licenseOle", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "consent1", "type": "ConsentPayment" }, "target": { "external_id": "cb2563", "type": "PaymentMethod" }, "type": "GRANTED" }, { "source": { "external_id": "companyParking", "type": "Company" }, "target": { "external_id": "applicationParking", "type": "Application" }, "type": "OWNS" }, { "source": { "external_id": "applicationParking", "type": "Application" }, "target": { "external_id": "consent1", "type": "ConsentPayment" }, "type": "USES" } ] } ``` ### Step 2a Policy JSON with multi-condition authorization: (1) license plate lookup, (2) loyalty plan consistency check, (3) consent validation with expiration check. Returns READ access to CreditCard.external_id when all conditions pass. **policy.json** ```json { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "_Application" }, "condition": { "cypher": "MATCH (subject:_Application) MATCH (app:Application)-[:USES]->(consentpayment:ConsentPayment)<-[:GRANTED]-(person:Person)-[:HAS]->(paymentmethod:PaymentMethod) MATCH (person)-[:IS_MEMBER]->(loyalty:Loyalty) MATCH (person)-[:OWNS]->(car:Car)-[:HAS]->(ln:LicenseNumber)", "filter": [ { "operator": "AND", "operands": [ { "attribute": "app.external_id", "operator": "=", "value": "$app_external_id" }, { "attribute": "subject.external_id", "operator": "=", "value": "$_appId" } ] } ] }, "allowed_reads": { "nodes": [ "ln.*", "app.*", "paymentmethod.external_id" ] } } ``` ### Step 2b POST request to create the loyalty authorization policy. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"meta\":{\"policy_version\":\"1.0-ciq\"},\"subject\":{\"type\":\"_Application\"},\"condition\":{\"cypher\":\"MATCH (subject:_Application) MATCH (app:Application)-[:USES]->(consentpayment:ConsentPayment)<-[:GRANTED]-(person:Person)-[:HAS]->(paymentmethod:PaymentMethod) MATCH (person)-[:IS_MEMBER]->(loyalty:Loyalty) MATCH (person)-[:OWNS]->(car:Car)-[:HAS]->(ln:LicenseNumber)\",\"filter\":[{\"operator\":\"AND\",\"operands\":[{\"attribute\":\"app.external_id\",\"operator\":\"=\",\"value\":\"$app_external_id\"},{\"attribute\":\"subject.external_id\",\"operator\":\"=\",\"value\":\"$_appId\"}]}]},\"allowed_reads\":{\"nodes\":[\"ln.*\",\"app.*\",\"paymentmethod.external_id\"]}}", "status": "ACTIVE", "tags": [] } ``` ### Step 2c GET request to verify the policy was created. **GET https://eu.api.indykite.com/configs/v1/authorization-policies/{policy_id}** ```json { "id": "your_policy_configuration_gid" } ``` ### Step 3a Query JSON that takes $licensePlate as input parameter, traverses the loyalty graph, and returns CreditCard.external_id for authorized requests. **knowledge_query.json** ```json { "nodes": [ "paymentmethod.external_id" ], "filter": { "attribute": "ln.property.number", "operator": "=", "value": "$license" } } ``` ### Step 3b POST request to create the loyalty query. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "description of knowledge query", "display_name": "knowledge query name", "name": "knowledge-query-name", "policy_id": "your_policy_gid", "query": "{\"nodes\":[\"paymentmethod.external_id\"],\"filter\":{\"attribute\":\"ln.property.number\",\"operator\":\"=\",\"value\":\"$license\"}}", "status": "ACTIVE" } ``` ### Step 3c GET request to verify the query was created. **GET https://eu.api.indykite.com/configs/v1/knowledge-queries/{query_id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 4a Execute the loyalty query with a license plate value. The system checks all authorization conditions before returning the credit card ID. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "knowledge_query_gid", "input_params": { "license": "AL98745", "app_external_id": "applicationParking" } } ``` ### Step 4b Response containing the credit card external_id. Empty result if authorization conditions are not met (e.g., expired consent, inconsistent loyalty plans). **response.json** ```json { "data": [ { "nodes": { "paymentmethod.external_id": "cb2563" } } ] } ``` ### Step 5a DELETE request to remove the query. **DELETE https://eu.api.indykite.com/configs/v1/knowledge-queries/{query_id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 5b DELETE request to remove the policy. **DELETE https://eu.api.indykite.com/configs/v1/authorization-policies/{policy_id}** ```json { "id": "your_policy_configuration_gid" } ``` ## Common Errors ### 401: UNAUTHENTICATED **Solution:** Check credentials: ServiceAccount Bearer token for config APIs, X-IK-ClientKey for execution ### 404: NOT_FOUND **Solution:** Verify the query/policy ID exists and belongs to your project --- Source: https://developer.indykite.com/resources/ciq-7 --- # ContX IQ: Connect System Nodes (_Application) to Business Data > Learn how to link auto-generated system nodes (_Application, _AppAgent) to your business data in the knowledge graph. This is essential for using the Application as an authorized subject in ContX IQ queries. **Category:** ContX IQ **API:** ContX IQ **Tags:** ContX IQ Policy, ContX IQ Query, Application Linking, _Application Node, System Integration, Write Query **Last Updated:** 2026-03-20 **OpenAPI Endpoints:** /capture/v1/nodes, /capture/v1/relationships, /configs/v1/authorization-policies, /configs/v1/knowledge-queries, /contx-iq/v1/execute **Related Guides:** /guides/guide-contx-iq, /guides/guide-sandbox ## Summary This example demonstrates how to connect system-generated nodes to business data: Background: When you create an Application and AppAgent in IndyKite: - _Application node is auto-created in the IKG - _AppAgent node is auto-created and linked to _Application Problem: These system nodes are isolated from your business data. Solution: Use a ContX IQ write query to create relationships: _Application -[HAS_AGREEMENT_WITH]-> Company After linking, the Application can: - Use itself as an authorized subject in queries - Traverse through business data relationships - Access data according to graph-based policies ## Use Case Scenario: Your Application needs to query data on behalf of Company1. System state after credential creation: - _Application(app-123) exists (auto-created) - _AppAgent(agent-456) exists (auto-created) - Company1, Person, Vehicle nodes exist (your data) - No connection between _Application and business data Linking operation: Create: _Application(app-123) -[HAS_AGREEMENT_WITH]-> Company1 After linking: - Queries with $_appId filter can match this Application - Application can traverse to Company1's data (Vehicles, Contracts, etc.) - Policies based on the HAS_AGREEMENT_WITH path now apply This is a foundational step for most ContX IQ scenarios. ## Requirements Prerequisites: - ServiceAccount credentials: For creating policies and queries (Bearer token) - AppAgent credentials: For data ingestion and query execution (X-IK-ClientKey) - Note: The _Application and _AppAgent nodes already exist after credential creation Required API access: - POST /capture/v1/nodes/ and /capture/v1/relationships/ (business data) - POST /configs/v1/authorization-policies (create linking policy) - POST /configs/v1/knowledge-queries (create linking query) - POST /contx-iq/v1/execute (execute linking) ## Steps Step 1: Ingest Business Data - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: POST Company, Person, Vehicle nodes and relationships - Result: Business graph ready (but not connected to _Application) Step 2: Create Linking Policy - Authentication: ServiceAccount credential (Bearer token) - Action: POST policy allowing _Application to create HAS_AGREEMENT_WITH relationships - Key detail: Uses $_appId filter which auto-resolves to the calling Application's ID - Result: Policy ID returned Step 3: Create Linking Query - Authentication: ServiceAccount credential (Bearer token) - Action: POST write query that creates _Application -[HAS_AGREEMENT_WITH]-> Company - The query uses $_appId to reference the subject Application - Result: Query ID returned Step 4: Execute Linking - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: POST to /contx-iq/v1/execute with the linking query - Result: _Application is now connected to Company in the graph Step 5: Cleanup - Action: DELETE query and policy (the relationship remains in the graph) ## Code Examples ### Step 1 Capture the nodes needed for this use case. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "alice", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "alice@email.com" }, { "type": "given_name", "value": "Alice" }, { "type": "last_name", "value": "Smith" } ] }, { "external_id": "ryan", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "ryan@yahoo.co.uk" }, { "type": "given_name", "value": "ryan" }, { "type": "last_name", "value": "mushu" } ] }, { "external_id": "tilda", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "tilda@yahoo.co.uk" }, { "type": "given_name", "value": "tilda" }, { "type": "last_name", "value": "mushu" } ] }, { "external_id": "cb123", "type": "PaymentMethod", "properties": [ { "type": "payment_name", "value": "Credit Card" } ] }, { "external_id": "kl123", "type": "PaymentMethod", "properties": [ { "type": "payment_name", "value": "Klarna" } ] }, { "external_id": "ct123", "type": "Contract", "properties": [ { "type": "category", "value": "Insurance" }, { "type": "status", "value": "Active" }, { "type": "number", "value": "hfgrten123", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "ct234", "type": "Contract", "properties": [ { "type": "category", "value": "Insurance" }, { "type": "status", "value": "Active" }, { "type": "number", "value": "hfgrten234", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "ct985", "type": "Contract", "properties": [ { "type": "category", "value": "Insurance" }, { "type": "status", "value": "Active" }, { "type": "number", "value": "hfgrten985", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "car1", "type": "Vehicle", "properties": [ { "type": "category", "value": "Car" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "rtfhcnvjt471" } ] }, { "external_id": "car2", "type": "Vehicle", "properties": [ { "type": "category", "value": "Car" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "kdcbfrt178" } ] }, { "external_id": "truck1", "type": "Vehicle", "properties": [ { "type": "category", "value": "Truck" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "sncnrkcldp" } ] }, { "external_id": "license1", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "AX123456", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "license2", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "OL123456", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "license3", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "VN123456", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "company1", "type": "Company", "properties": [ { "type": "name", "value": "Company1" }, { "type": "registration", "value": "256314523" } ] }, { "external_id": "company2", "type": "Company", "properties": [ { "type": "name", "value": "Company2" }, { "type": "registration", "value": "942365123" } ] }, { "external_id": "application1", "type": "Application", "properties": [ { "type": "name", "value": "Application" } ] }, { "external_id": "application2", "type": "Application", "properties": [ { "type": "name", "value": "Application2" } ] } ] } ``` Capture the relationships needed for this use case. **POST https://eu.api.indykite.com/capture/v1/relationships/** ```json { "relationships": [ { "source": { "external_id": "ryan", "type": "Person" }, "target": { "external_id": "cb123", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "tilda", "type": "Person" }, "target": { "external_id": "kl123", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "cb123", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "ryan", "type": "Person" }, "target": { "external_id": "ct123", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "tilda", "type": "Person" }, "target": { "external_id": "ct234", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "ct985", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "ct123", "type": "Contract" }, "target": { "external_id": "car1", "type": "Vehicle" }, "type": "COVERS" }, { "source": { "external_id": "ct985", "type": "Contract" }, "target": { "external_id": "car1", "type": "Vehicle" }, "type": "COVERS" }, { "source": { "external_id": "ct234", "type": "Contract" }, "target": { "external_id": "truck1", "type": "Vehicle" }, "type": "COVERS" }, { "source": { "external_id": "car1", "type": "Vehicle" }, "target": { "external_id": "license1", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "truck1", "type": "Vehicle" }, "target": { "external_id": "license2", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "car2", "type": "Vehicle" }, "target": { "external_id": "license3", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "company1", "type": "Company" }, "target": { "external_id": "car1", "type": "Vehicle" }, "type": "OWNS" }, { "source": { "external_id": "company1", "type": "Company" }, "target": { "external_id": "car2", "type": "Vehicle" }, "type": "OWNS" }, { "source": { "external_id": "company1", "type": "Company" }, "target": { "external_id": "truck1", "type": "Vehicle" }, "type": "OWNS" }, { "source": { "external_id": "application1", "type": "Application" }, "target": { "external_id": "company1", "type": "Company" }, "type": "HAS_AGREEMENT_WITH" }, { "source": { "external_id": "application2", "type": "Application" }, "target": { "external_id": "company1", "type": "Company" }, "type": "HAS_AGREEMENT_WITH" } ] } ``` ### Step 2 Policy which designates the derived query can create the HAS_AGREEMENT_WITH relationship. **policy.json** ```json { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "_Application" }, "condition": { "cypher": "MATCH (subject:_Application) MATCH (company:Company)-[r2:OWNS]->(vehicle:Vehicle)", "filter": [ { "operator": "AND", "operands": [ { "attribute": "subject.external_id", "operator": "=", "value": "$_appId" }, { "attribute": "company.external_id", "operator": "=", "value": "$companyID" } ] } ] }, "allowed_upserts": { "relationships": { "relationship_types": [ { "type": "HAS_AGREEMENT_WITH", "source_node_label": "_Application", "target_node_label": "Company" } ] } }, "allowed_reads": { "nodes": [ "company.*", "subject.*" ] } } ``` Request to create a CIQ Policy configuration using REST. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"meta\":{\"policy_version\":\"1.0-ciq\"},\"subject\":{\"type\":\"_Application\"},\"condition\":{\"cypher\":\"MATCH (subject:_Application) MATCH (company:Company)-[r2:OWNS]->(vehicle:Vehicle)\",\"filter\":[{\"operator\":\"AND\",\"operands\":[{\"attribute\":\"subject.external_id\",\"operator\":\"=\",\"value\":\"$_appId\"},{\"attribute\":\"company.external_id\",\"operator\":\"=\",\"value\":\"$companyID\"}]}]},\"allowed_upserts\":{\"relationships\":{\"relationship_types\":[{\"type\":\"HAS_AGREEMENT_WITH\",\"source_node_label\":\"_Application\",\"target_node_label\":\"Company\"}]}},\"allowed_reads\":{\"nodes\":[\"company.*\",\"subject.*\"]}}", "status": "ACTIVE", "tags": [] } ``` Request to read the CIQ Policy configuration using REST. **policy_request.json** ```json { "id": "your_policy_configuration_gid" } ``` ### Step 3 The CIQ Query designates the relationship to upsert. **knowledge_query.json** ```json { "nodes": [ "subject.external_id" ], "relationships": [ "r1" ], "upsert_relationships": [ { "name": "r1", "source": "subject", "target": "company", "type": "HAS_AGREEMENT_WITH" } ] } ``` Request to create a CIQ Query configuration using REST. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "description of knowledge query", "display_name": "knowledge query name", "name": "knowledge-query-name", "policy_id": "your_policy_gid", "query": "{\"nodes\":[\"subject.external_id\"],\"relationships\":[\"r1\"],\"upsert_relationships\":[{\"name\":\"r1\",\"source\":\"subject\",\"target\":\"company\",\"type\":\"HAS_AGREEMENT_WITH\"}]}", "status": "ACTIVE" } ``` Read the CIQ Query Configuration. **GET https://eu.api.indykite.com/configs/v1/knowledge-queries/{id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 4 CIQ Execution request in json format. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "ciq_query_gid", "input_params": { "companyID": "company1" } } ``` CIQ Execution response in json format. **response.json** ```json { "data": [ { "nodes": { "subject.external_id": "application_external_id" }, "relationships": { "r1": { "Id": 1152932499723124700, "ElementId": "5:3a2b09d5-2923-45d7-8453-8b1c698427b0:1152932499723124736", "StartId": 0, "StartElementId": "4:3a2b09d5-2923-45d7-8453-8b1c698427b0:0", "EndId": 15, "EndElementId": "4:3a2b09d5-2923-45d7-8453-8b1c698427b0:15", "Type": "HAS_AGREEMENT_WITH", "Props": { "create_time": "2025-06-09T15:12:46.374Z", "id": "48BJHS2CTFKcVpD4cUF8IA", "update_time": "2025-06-09T15:12:46.374Z" } } } }, { "nodes": { "subject.external_id": "application_external_id" }, "relationships": { "r1": { "Id": 1152932499723124700, "ElementId": "5:3a2b09d5-2923-45d7-8453-8b1c698427b0:1152932499723124736", "StartId": 0, "StartElementId": "4:3a2b09d5-2923-45d7-8453-8b1c698427b0:0", "EndId": 15, "EndElementId": "4:3a2b09d5-2923-45d7-8453-8b1c698427b0:15", "Type": "HAS_AGREEMENT_WITH", "Props": { "create_time": "2025-06-09T15:12:46.374Z", "id": "48BJHS2CTFKcVpD4cUF8IA", "update_time": "2025-06-09T15:12:46.374Z" } } } }, { "nodes": { "subject.external_id": "application_external_id" }, "relationships": { "r1": { "Id": 1152932499723124700, "ElementId": "5:3a2b09d5-2923-45d7-8453-8b1c698427b0:1152932499723124736", "StartId": 0, "StartElementId": "4:3a2b09d5-2923-45d7-8453-8b1c698427b0:0", "EndId": 15, "EndElementId": "4:3a2b09d5-2923-45d7-8453-8b1c698427b0:15", "Type": "HAS_AGREEMENT_WITH", "Props": { "create_time": "2025-06-09T15:12:46.374Z", "id": "48BJHS2CTFKcVpD4cUF8IA", "update_time": "2025-06-09T15:12:46.374Z" } } } } ] } ``` ### Step 5 Delete the CIQ Query. **DELETE https://eu.api.indykite.com/configs/v1/knowledge-queries/{id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` Delete the CIQ Policy. **DELETE https://eu.api.indykite.com/configs/v1/authorization-policies/{id}** ```json { "id": "your_policy_configuration_gid" } ``` ## Common Errors ### 401: UNAUTHENTICATED **Solution:** Check credentials: ServiceAccount Bearer token for config APIs, X-IK-ClientKey for execution ### 404: NOT_FOUND **Solution:** Verify the query/policy ID exists and belongs to your project --- Source: https://developer.indykite.com/resources/ciq-8 --- # ContX IQ: Complete CRUD Workflow with _Application as Subject > Comprehensive example showing how an Application can read resources, create relationships to those resources, and then create new nodes. Demonstrates progressive authorization as the graph structure changes. **Category:** ContX IQ **API:** ContX IQ **Tags:** ContX IQ Policy, ContX IQ Query, CRUD Operations, _Application Subject, Progressive Authorization, Read and Write **Last Updated:** 2026-03-20 **OpenAPI Endpoints:** /capture/v1/nodes, /capture/v1/relationships, /configs/v1/authorization-policies, /configs/v1/knowledge-queries, /contx-iq/v1/execute **Related Guides:** /guides/guide-contx-iq, /guides/guide-sandbox ## Summary This comprehensive example demonstrates a full CRUD workflow: Phase 1 - Initial Read: - Application reads all Car nodes (allowed by unrestricted policy) Phase 2 - Create Relationship: - Application creates USES relationship to a specific Car - Graph: _Application -[USES]-> Car1 Phase 3 - Read with Relationship Filter: - New policy restricts reads to Cars the Application USES - Only Car1 is returned (not all cars) Phase 4 - Create New Node: - Application creates a new Car node - Graph: _Application -[USES]-> Car1, _Application -[USES]-> NewCar Key concept: As relationships are added, authorization scope changes dynamically. ## Use Case Scenario: A fleet management application that gradually acquires access to vehicles. Workflow demonstration: 1. Application starts with no vehicle relationships 2. Application reads available vehicles (broad access) 3. Application "claims" Car1 by creating USES relationship 4. Future reads are scoped to claimed vehicles only 5. Application can add new vehicles to its fleet Graph evolution: Initial: _Application (no connections to data) After Step 6: _Application -[USES]-> Car1 After Step 12: _Application -[USES]-> Car1, _Application -[USES]-> NewCar This pattern is common for: - Multi-tenant applications - Resource provisioning systems - Dynamic access control scenarios ## Requirements Prerequisites: - ServiceAccount credentials: For creating policies and queries (Bearer token) - AppAgent credentials: For data ingestion and query execution (X-IK-ClientKey) Required API access: - POST /capture/v1/nodes/ and /capture/v1/relationships/ (initial data) - POST /configs/v1/authorization-policies (multiple policies for different phases) - POST /configs/v1/knowledge-queries (read and write queries) - POST /contx-iq/v1/execute (run queries) ## Steps Step 1: Ingest Initial Car Data - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: POST Car nodes (Car1, Car2, Car3) and Person nodes - Result: Graph with vehicles ready for Application to interact with Step 2: Create Initial Read/Write Policy - Authentication: ServiceAccount credential (Bearer token) - Action: POST policy allowing: - READ on Car nodes (filtered by $carId parameter) - UPSERT on USES relationships (Application -> Car) - Result: Policy ID returned Step 3: Create Read Query - Authentication: ServiceAccount credential (Bearer token) - Action: POST query that reads Car node properties - Input parameter: $carId (optional, to filter specific car) - Result: Query ID returned Step 4: Execute Read Query - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: Run query to read Car1 - Result: Car1 properties returned Step 5: Create Relationship Upsert Query - Authentication: ServiceAccount credential (Bearer token) - Action: POST query that creates _Application -[USES]-> Car relationship - Result: Query ID returned Step 6: Execute Relationship Creation - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: Run query to create USES relationship to Car1 - Result: _Application now connected to Car1 Step 7: Create Relationship-Scoped Policy - Authentication: ServiceAccount credential (Bearer token) - Action: POST policy that only allows READ on Cars the Application USES - This demonstrates progressive authorization based on relationships - Result: Policy ID returned Step 8: Create Scoped Read Query - Authentication: ServiceAccount credential (Bearer token) - Action: POST query that reads Cars connected to Application via USES - Result: Query ID returned Step 9: Execute Scoped Read - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: Run query - Result: Only Car1 returned (the one with USES relationship) Step 10: Create Node Creation Policy - Authentication: ServiceAccount credential (Bearer token) - Action: POST policy allowing creation of new Car nodes - Result: Policy ID returned Step 11: Create Node Creation Query - Authentication: ServiceAccount credential (Bearer token) - Action: POST query that creates new Car node and USES relationship - Parameters: $newCarId, $newCarName - Result: Query ID returned Step 12: Execute Node Creation - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: Run query with new car parameters - Result: New Car node created and linked to Application ## Code Examples ### Step 1 Capture the nodes needed for this use case. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "alice", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "alice@email.com" }, { "type": "given_name", "value": "Alice" }, { "type": "last_name", "value": "Smith" } ] }, { "external_id": "knightrider", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "knightrider@demo.com" }, { "type": "name", "value": "Michael Knight" } ] }, { "external_id": "satchmo", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "satchmo@demo.com" }, { "type": "name", "value": "Louis Armstrong" } ] }, { "external_id": "karel", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "karel@demo.com" }, { "type": "name", "value": "Karel Plihal" } ] }, { "external_id": "kitt", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "pontiac" }, { "type": "model", "value": "Firebird" } ] }, { "external_id": "cadillacv16", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "Cadillac" }, { "type": "model", "value": "V-16" } ] }, { "external_id": "harmonika", "type": "Bus", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "Ikarus" }, { "type": "model", "value": "280" } ] }, { "external_id": "listek", "type": "Ticket", "is_identity": false }, { "external_id": "airbook-xyz", "type": "Laptop", "is_identity": false } ] } ``` Capture the relationships needed for this use case. **POST https://eu.api.indykite.com/capture/v1/relationships/** ```json { "relationships": [ { "source": { "external_id": "knightrider", "type": "Person" }, "target": { "external_id": "kitt", "type": "Car" }, "type": "DRIVES" }, { "source": { "external_id": "satchmo", "type": "Person" }, "target": { "external_id": "cadillacv16", "type": "Car" }, "type": "DRIVES" }, { "source": { "external_id": "karel", "type": "Person" }, "target": { "external_id": "listek", "type": "Ticket" }, "type": "HAS" }, { "source": { "external_id": "listek", "type": "Ticket" }, "target": { "external_id": "harmonika", "type": "Bus" }, "type": "FOR" }, { "source": { "external_id": "karel", "type": "Person" }, "target": { "external_id": "airbook-xyz", "type": "Laptop" }, "type": "OWNS" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "airbook-xyz", "type": "Laptop" }, "type": "OWNS" }, { "source": { "external_id": "knightrider", "type": "Person" }, "target": { "external_id": "kitt", "type": "Car" }, "type": "OWNS" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "cadillacv16", "type": "Car" }, "type": "DRIVES" } ] } ``` ### Step 2 Create a CIQ Policy which designates the nodes which are allowed to be read and the nodes which are allowed to be upserted. The policy filters first on subject.external_id. When the subject is _Application, the $_appId input does not need to be provided and is automatically assigned the subject.external_id value. The policy also filters on car.external_id. The $carId input will need to be provided in the CIQ Execution. The policy allows the Car nodes to be read, existing Car nodes to be upserted and new relationships between the subject and Car nodes to be upserted **policy.json** ```json { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "_Application" }, "condition": { "cypher": "MATCH (subject:_Application) MATCH (car:Car)", "filter": [ { "operator": "AND", "operands": [ { "operator": "=", "attribute": "subject.external_id", "value": "$_appId", "advice": { "error_description": "subject external_id filter error" } }, { "operator": "=", "attribute": "car.external_id", "value": "$carId" } ] } ] }, "allowed_reads": { "nodes": [ "car", "car.*" ] }, "allowed_upserts": { "nodes": { "existing_nodes": [ "car" ] }, "relationships": { "relationship_types": [ { "type": "USES", "source_node_label": "_Application", "target_node_label": "Car" } ] } } } ``` Json to create the CIQ Policy configuration using REST. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"meta\":{\"policy_version\":\"1.0-ciq\"},\"subject\":{\"type\":\"_Application\"},\"condition\":{\"cypher\":\"MATCH (subject:_Application) MATCH (car:Car)\",\"filter\":[{\"operator\":\"AND\",\"operands\":[{\"operator\":\"=\",\"attribute\":\"subject.external_id\",\"value\":\"$_appId\",\"advice\":{\"error_description\":\"subject external_id filter error\"}},{\"operator\":\"=\",\"attribute\":\"car.external_id\",\"value\":\"$carId\"}]}]},\"allowed_reads\":{\"nodes\":[\"car\",\"car.*\"]},\"allowed_upserts\":{\"nodes\":{\"existing_nodes\":[\"car\"]},\"relationships\":{\"relationship_types\":[{\"type\":\"USES\",\"source_node_label\":\"_Application\",\"target_node_label\":\"Car\"}]}}}", "status": "ACTIVE", "tags": [] } ``` Json to read the CIQ Policy configuration using REST. **policy_request.json** ```json { "id": "your_policy_configuration_gid" } ``` ### Step 3 The CIQ Query designates the Car nodes to be read. **knowledge_query.json** ```json { "nodes": [ "car.external_id", "car.property.model" ], "relationships": [] } ``` Json to create a CIQ Query configuration using REST. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "description of knowledge query", "display_name": "knowledge query name", "name": "knowledge-query-name", "policy_id": "your_policy_gid", "query": "{\"nodes\":[\"car.external_id\",\"car.property.model\"],\"relationships\":[]}", "status": "ACTIVE" } ``` Read the CIQ Query Configuration. **GET https://eu.api.indykite.com/configs/v1/knowledge-queries/{id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 4 Run a CIQ Execution to read a Car node. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "knowledge_query_gid", "input_params": { "carId": "cadillacv16" }, "page_token": 1 } ``` CIQ Execution response. **response.json** ```json { "data": [ { "nodes": { "car.external_id": "cadillacv16", "car.property.model": "V-16" } } ] } ``` ### Step 5 The CIQ Query designates the relationships USES to be upserted between the subject and a Car node. **knowledge_query.json** ```json { "nodes": [ "car.external_id", "car.property.model" ], "relationships": [], "upsert_relationships": [ { "name": "newRel", "source": "subject", "target": "car", "type": "USES", "properties": [ { "type": "status", "value": "active" } ] } ] } ``` Json to create a CIQ Query configuration using REST. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "description of knowledge query", "display_name": "knowledge query name", "name": "knowledge-query-name", "policy_id": "your_policy_gid", "query": "{\"nodes\":[\"car.external_id\",\"car.property.model\"],\"relationships\":[],\"upsert_relationships\":[{\"name\":\"newRel\",\"source\":\"subject\",\"target\":\"car\",\"type\":\"USES\",\"properties\":[{\"type\":\"status\",\"value\":\"active\"}]}]}", "status": "ACTIVE" } ``` Read the CIQ Query Configuration. **GET https://eu.api.indykite.com/configs/v1/knowledge-queries/{id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 6 Run a CIQ Execution to upsert one relationship USES between the subject and a Car node. **ciq.json** ```json { "id": "knowledge_query_gid", "input_params": { "carId": "cadillacv16" }, "page_token": 1 } ``` CIQ Execution response. **response.json** ```json { "data": [ { "nodes": { "car.external_id": "cadillacv16", "car.property.model": "V-16" } } ] } ``` ### Step 7 Create a CIQ Policy which designates the nodes which are allowed to be read and the nodes which are allowed to be upserted, according to the graph created previously. The policy filters first on subject.external_id. When the subject is _Application, the $_appId input does not need to be provided and is automatically assigned the subject.external_id value. The policy also filters on car.external_id. The $carId input will need to be provided in the CIQ Execution. The policy allows the Car nodes to be read, existing Car nodes to be upserted and new relationships between the subject and Car nodes to be upserted **policy.json** ```json { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "_Application" }, "condition": { "cypher": "MATCH (subject:_Application)-[:USES]->(car:Car)", "filter": [ { "operator": "AND", "operands": [ { "operator": "=", "attribute": "subject.external_id", "value": "$_appId", "advice": { "error_description": "subject external_id filter error" } }, { "operator": "=", "attribute": "car.external_id", "value": "$carId" } ] } ] }, "allowed_reads": { "nodes": [ "car", "car.*" ] }, "allowed_upserts": { "nodes": { "existing_nodes": [ "car" ] }, "relationships": { "relationship_types": [ { "type": "USES", "source_node_label": "_Application", "target_node_label": "Car" } ] } } } ``` Request to create the CIQ Policy configuration using REST. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"meta\":{\"policy_version\":\"1.0-ciq\"},\"subject\":{\"type\":\"_Application\"},\"condition\":{\"cypher\":\"MATCH (subject:_Application)-[:USES]->(car:Car)\",\"filter\":[{\"operator\":\"AND\",\"operands\":[{\"operator\":\"=\",\"attribute\":\"subject.external_id\",\"value\":\"$_appId\",\"advice\":{\"error_description\":\"subject external_id filter error\"}},{\"operator\":\"=\",\"attribute\":\"car.external_id\",\"value\":\"$carId\"}]}]},\"allowed_reads\":{\"nodes\":[\"car\",\"car.*\"]},\"allowed_upserts\":{\"nodes\":{\"existing_nodes\":[\"car\"]},\"relationships\":{\"relationship_types\":[{\"type\":\"USES\",\"source_node_label\":\"_Application\",\"target_node_label\":\"Car\"}]}}}", "status": "ACTIVE", "tags": [] } ``` Request to read the CIQ Policy configuration using REST. **policy_request.json** ```json { "id": "your_policy_configuration_gid" } ``` ### Step 8 The CIQ Query designates the Car nodes to be read. **knowledge_query.json** ```json { "nodes": [ "car.external_id", "car.property.model" ], "relationships": [] } ``` Json to create a CIQ Query configuration using REST. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "description of knowledge query", "display_name": "knowledge query name", "name": "knowledge-query-name", "policy_id": "your_policy_gid", "query": "{\"nodes\":[\"car.external_id\",\"car.property.model\"],\"relationships\":[]}", "status": "ACTIVE" } ``` Read the CIQ Query Configuration. **GET https://eu.api.indykite.com/configs/v1/knowledge-queries/{id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 9 Run a CIQ Execution to read a Car node. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "knowledge_query_gid", "input_params": { "carId": "cadillacv16" }, "page_token": 1 } ``` CIQ Execution response in json format. **response.json** ```json { "data": [ { "nodes": { "car.external_id": "cadillacv16", "car.property.model": "V-16" } } ] } ``` ### Step 10 Create a CIQ Policy which designates only the subject _Application and then the Car nodes which are allowed to be upserted. The policy only filters first on subject.external_id. When the subject is _Application, the $_appId input does not need to be provided and is automatically assigned the subject.external_id value. The policy allows the subject node to be read, new Car nodes to be upserted and new relationships between the subject and Car nodes to be upserted **policy.json** ```json { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "_Application" }, "condition": { "cypher": "MATCH (subject:_Application)", "filter": [ { "operator": "=", "attribute": "subject.external_id", "value": "$_appId" } ] }, "allowed_reads": { "nodes": [ "subject", "subject.*" ] }, "allowed_upserts": { "nodes": { "node_types": [ "Car" ] }, "relationships": { "relationship_types": [ { "type": "USES", "source_node_label": "_Application", "target_node_label": "Car" } ] } } } ``` Json to create the CIQ Policy configuration using REST. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"meta\":{\"policy_version\":\"1.0-ciq\"},\"subject\":{\"type\":\"_Application\"},\"condition\":{\"cypher\":\"MATCH (subject:_Application)\",\"filter\":[{\"operator\":\"=\",\"attribute\":\"subject.external_id\",\"value\":\"$_appId\"}]},\"allowed_reads\":{\"nodes\":[\"subject\",\"subject.*\"]},\"allowed_upserts\":{\"nodes\":{\"node_types\":[\"Car\"]},\"relationships\":{\"relationship_types\":[{\"type\":\"USES\",\"source_node_label\":\"_Application\",\"target_node_label\":\"Car\"}]}}}", "status": "ACTIVE", "tags": [] } ``` Json to read the CIQ Policy configuration using REST. **policy_request.json** ```json { "id": "your_policy_configuration_gid" } ``` ### Step 11 The CIQ Query designates the Car nodes to be upserted. **knowledge_query.json** ```json { "nodes": [ "car.external_id", "car.property.model" ], "relationships": [], "upsert_nodes": [ { "name": "car", "type": "Car", "external_id": "$carExtId", "properties": [ { "type": "model", "value": "$model" }, { "type": "color", "value": "$color" } ] } ], "upsert_relationships": [ { "name": "newRel", "source": "subject", "target": "car", "type": "USES", "properties": [ { "type": "status", "value": "active" } ] } ] } ``` Json to create a CIQ Query configuration using REST. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "description of knowledge query", "display_name": "knowledge query name", "name": "knowledge-query-name", "policy_id": "your_policy_gid", "query": "{\"nodes\":[\"car.external_id\",\"car.property.model\"],\"relationships\":[],\"upsert_nodes\":[{\"name\":\"car\",\"type\":\"Car\",\"external_id\":\"$carExtId\",\"properties\":[{\"type\":\"model\",\"value\":\"$model\"},{\"type\":\"color\",\"value\":\"$color\"}]}],\"upsert_relationships\":[{\"name\":\"newRel\",\"source\":\"subject\",\"target\":\"car\",\"type\":\"USES\",\"properties\":[{\"type\":\"status\",\"value\":\"active\"}]}]}", "status": "ACTIVE" } ``` Read the CIQ Query Configuration. **GET https://eu.api.indykite.com/configs/v1/knowledge-queries/{id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 12 Run a CIQ Execution to upsert a Car node. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "knowledge_query_gid", "input_params": { "carExtId": "LightningMcqueen", "model": "Corvette C6", "color": "red" }, "page_token": 1 } ``` CIQ Execution response. **response.json** ```json { "data": [ { "nodes": { "car.external_id": "LightningMcqueen", "car.property.model": "Corvette C6" } } ] } ``` ## Common Errors ### 401: UNAUTHENTICATED **Solution:** Check credentials: ServiceAccount Bearer token for config APIs, X-IK-ClientKey for execution ### 404: NOT_FOUND **Solution:** Verify the query/policy ID exists and belongs to your project --- Source: https://developer.indykite.com/resources/ciq-9 --- # ContX IQ: Query License Numbers a Person Can Access > Query the IndyKite Knowledge Graph (IKG) to retrieve all vehicle license numbers that a specific person is authorized to view based on their contractual relationships. **Category:** ContX IQ **API:** ContX IQ **Tags:** ContX IQ Policy, ContX IQ Query, ContX IQ Execution, Read Authorization, Graph Traversal **Last Updated:** 2026-03-20 **OpenAPI Endpoints:** /capture/v1/nodes, /capture/v1/relationships, /configs/v1/authorization-policies, /configs/v1/knowledge-queries, /contx-iq/v1/execute **Related Guides:** /guides/guide-contx-iq, /guides/guide-sandbox ## Summary This example demonstrates how to: 1. Create an authorization policy that defines read access to LicenseNumber nodes 2. Create a query that retrieves license numbers based on the policy 3. Execute the query to get results filtered by authorization rules The policy grants a Person access to LicenseNumber nodes when a path exists: Person -> ACCEPTED -> Contract -> COVERS -> Vehicle -> HAS -> LicenseNumber. ## Use Case Scenario: Ryan (a Person node) has accepted Contract1, which covers Vehicle "Car1" owned by Company1. Car1 has LicenseNumber "ABC-123". Goal: Ryan wants to retrieve all license numbers for vehicles he is contractually allowed to use. Expected result: The query returns "ABC-123" because the relationship path exists: Ryan -> ACCEPTED -> Contract1 -> COVERS -> Car1 -> HAS -> ABC-123 ## Requirements Prerequisites: - ServiceAccount credentials: Created in IndyKite Hub for your organization (used for policy/query configuration) - AppAgent credentials: Created in IndyKite Hub for your Project/Application (used for data ingestion and query execution) Required API access: - POST /capture/v1/nodes/ (capture nodes) - POST /capture/v1/relationships/ (capture relationships) - POST /configs/v1/authorization-policies (create policy) - POST /configs/v1/knowledge-queries (create query) - POST /contx-iq/v1/execute (run query) ## Steps Step 1: Ingest Graph Data - Authentication: AppAgent credential as API key (header: X-IK-ClientKey) - Action: POST nodes and relationships to build the graph - Result: Person, Contract, Vehicle, LicenseNumber, and Company nodes created with relationships Step 2: Create Authorization Policy - Authentication: ServiceAccount credential as Bearer token - Action: POST policy configuration to /configs/v1/authorization-policies - Policy logic: Grants READ access to LicenseNumber nodes when the subject (_Application) connects through the relationship path - Key parameter: Filter uses $_appId (auto-populated system variable for the calling Application's ID) - Result: Policy ID returned for reference Step 3: Create ContX IQ Query - Authentication: ServiceAccount credential as Bearer token - Action: POST query configuration to /configs/v1/knowledge-queries - Query logic: Traverses from Person (matched by email) through relationships to return LicenseNumber values - Result: Query ID returned for reference Step 4: Execute Query - Authentication: AppAgent credential as API key (header: X-IK-ClientKey) - Action: POST to /contx-iq/v1/execute with query ID and input parameters - Input parameter: email (e.g., "ryan@example.com") - Result: Array of license numbers the person can access Step 5: Cleanup - Action: DELETE the query and policy configurations - Note: This does not delete ingested graph data ## Code Examples ### Step 1a Capture nodes into the IKG. Creates Person, Company, Vehicle, Contract, LicenseNumber, and PaymentMethod nodes with their properties. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "alice", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "alice@email.com" }, { "type": "given_name", "value": "Alice" }, { "type": "last_name", "value": "Smith" } ] }, { "external_id": "ryan", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "ryan@yahoo.co.uk" }, { "type": "given_name", "value": "ryan" }, { "type": "last_name", "value": "mushu" } ] }, { "external_id": "tilda", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "tilda@yahoo.co.uk" }, { "type": "given_name", "value": "tilda" }, { "type": "last_name", "value": "mushu" } ] }, { "external_id": "cb123", "type": "PaymentMethod", "properties": [ { "type": "payment_name", "value": "Credit Card" } ] }, { "external_id": "kl123", "type": "PaymentMethod", "properties": [ { "type": "payment_name", "value": "Klarna" } ] }, { "external_id": "ct123", "type": "Contract", "properties": [ { "type": "category", "value": "Insurance" }, { "type": "status", "value": "Active" }, { "type": "number", "value": "hfgrten123", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "ct234", "type": "Contract", "properties": [ { "type": "category", "value": "Insurance" }, { "type": "status", "value": "Active" }, { "type": "number", "value": "hfgrten234", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "ct985", "type": "Contract", "properties": [ { "type": "category", "value": "Insurance" }, { "type": "status", "value": "Active" }, { "type": "number", "value": "hfgrten985", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "car1", "type": "Vehicle", "properties": [ { "type": "category", "value": "Car" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "rtfhcnvjt471" } ] }, { "external_id": "car2", "type": "Vehicle", "properties": [ { "type": "category", "value": "Car" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "kdcbfrt178" } ] }, { "external_id": "truck1", "type": "Vehicle", "properties": [ { "type": "category", "value": "Truck" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "sncnrkcldp" } ] }, { "external_id": "license1", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "AX123456", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "license2", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "OL123456", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "license3", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "VN123456", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "company1", "type": "Company", "properties": [ { "type": "name", "value": "Company1" }, { "type": "registration", "value": "256314523" } ] }, { "external_id": "company2", "type": "Company", "properties": [ { "type": "name", "value": "Company2" }, { "type": "registration", "value": "942365123" } ] }, { "external_id": "application1", "type": "Application", "properties": [ { "type": "name", "value": "Application" } ] }, { "external_id": "application2", "type": "Application", "properties": [ { "type": "name", "value": "Application2" } ] } ] } ``` ### Step 1b Capture relationships between nodes. Creates ACCEPTED, COVERS, OWNS, HAS, and HAS_AGREEMENT_WITH relationship types. **POST https://eu.api.indykite.com/capture/v1/relationships/** ```json { "relationships": [ { "source": { "external_id": "ryan", "type": "Person" }, "target": { "external_id": "cb123", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "tilda", "type": "Person" }, "target": { "external_id": "kl123", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "cb123", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "ryan", "type": "Person" }, "target": { "external_id": "ct123", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "tilda", "type": "Person" }, "target": { "external_id": "ct234", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "ct985", "type": "Contract" }, "type": "ACCEPTED" }, { "source": { "external_id": "ct123", "type": "Contract" }, "target": { "external_id": "car1", "type": "Vehicle" }, "type": "COVERS" }, { "source": { "external_id": "ct985", "type": "Contract" }, "target": { "external_id": "car1", "type": "Vehicle" }, "type": "COVERS" }, { "source": { "external_id": "ct234", "type": "Contract" }, "target": { "external_id": "truck1", "type": "Vehicle" }, "type": "COVERS" }, { "source": { "external_id": "car1", "type": "Vehicle" }, "target": { "external_id": "license1", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "truck1", "type": "Vehicle" }, "target": { "external_id": "license2", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "car2", "type": "Vehicle" }, "target": { "external_id": "license3", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "company1", "type": "Company" }, "target": { "external_id": "car1", "type": "Vehicle" }, "type": "OWNS" }, { "source": { "external_id": "company1", "type": "Company" }, "target": { "external_id": "car2", "type": "Vehicle" }, "type": "OWNS" }, { "source": { "external_id": "company1", "type": "Company" }, "target": { "external_id": "truck1", "type": "Vehicle" }, "type": "OWNS" }, { "source": { "external_id": "application1", "type": "Application" }, "target": { "external_id": "company1", "type": "Company" }, "type": "HAS_AGREEMENT_WITH" }, { "source": { "external_id": "application2", "type": "Application" }, "target": { "external_id": "company1", "type": "Company" }, "type": "HAS_AGREEMENT_WITH" } ] } ``` ### Step 2a Authorization policy JSON. Defines that subjects matching the _Application type can READ LicenseNumber nodes when connected via Person -> Contract -> Vehicle -> LicenseNumber path. **policy.json** ```json { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "_Application" }, "condition": { "cypher": "MATCH (subject:_Application) MATCH (person:Person)-[r1:ACCEPTED]->(contract:Contract)-[r2:COVERS]->(vehicle:Vehicle)-[r3:HAS]->(ln:LicenseNumber)", "filter": [ { "operator": "AND", "operands": [ { "attribute": "person.property.email", "operator": "=", "value": "$person_email" }, { "attribute": "subject.external_id", "operator": "=", "value": "$_appId" } ] } ] }, "allowed_reads": { "nodes": [ "ln.property.number", "ln.property.transferrable" ], "relationships": [] } } ``` ### Step 2b POST request body to create the authorization policy. Wraps the policy JSON with location (project ID) and metadata. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"meta\":{\"policy_version\":\"1.0-ciq\"},\"subject\":{\"type\":\"_Application\"},\"condition\":{\"cypher\":\"MATCH (subject:_Application) MATCH (person:Person)-[r1:ACCEPTED]->(contract:Contract)-[r2:COVERS]->(vehicle:Vehicle)-[r3:HAS]->(ln:LicenseNumber)\",\"filter\":[{\"operator\":\"AND\",\"operands\":[{\"attribute\":\"person.property.email\",\"operator\":\"=\",\"value\":\"$person_email\"},{\"attribute\":\"subject.external_id\",\"operator\":\"=\",\"value\":\"$_appId\"}]}]},\"allowed_reads\":{\"nodes\":[\"ln.property.number\",\"ln.property.transferrable\"],\"relationships\":[]}}", "status": "ACTIVE", "tags": [] } ``` ### Step 2b (Python) Python SDK equivalent: Creates the same authorization policy using the IndyKite Python client library. **create_policy.py** ```python import http.client conn = http.client.HTTPSConnection("eu.api.indykite.com") payload = "{"description": "", "display_name": "", "name": "", "policy": "", "project_id": "", "status": "ACTIVE", "tags": [ "" ]}" headers = { 'Content-Type': "application/json", 'Authorization': "YOUR_SECRET_TOKEN" } conn.request("POST", "/configs/v1/authorization-policies", payload, headers) res = conn.getresponse() data = res.read() ``` ### Step 2c GET request to verify the policy was created. Returns the full policy configuration including system-generated ID and timestamps. **GET https://eu.api.indykite.com/configs/v1/authorization-policies/{policy_id}** ```json { "id": "your_policy_configuration_gid" } ``` ### Step 2c (Python) Python SDK equivalent: Reads the policy configuration to verify creation. **read_policy.py** ```python import http.client conn = http.client.HTTPSConnection("eu.api.indykite.com") headers = { 'Authorization': "YOUR_SECRET_TOKEN" } conn.request("GET", "/configs/v1/authorization-policies/{{id}}", headers=headers) res = conn.getresponse() data = res.read() ``` ### Step 3a Knowledge query JSON. Defines a Cypher-like query that: (1) matches a Person by email input parameter, (2) traverses to LicenseNumber nodes via contracts, (3) returns license number values. **knowledge_query.json** ```json { "nodes": [ "ln.property.number" ], "filter": { "attribute": "ln.property.number", "operator": "=", "value": "$ln_number" } } ``` ### Step 3b POST request body to create the knowledge query. Links the query to the authorization policy created in Step 2. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "description of knowledge query", "display_name": "knowledge query name", "name": "knowledge-query-name", "policy_id": "your_policy_gid", "query": "{\"nodes\":[\"ln.property.number\"],\"filter\":{\"attribute\":\"ln.property.number\",\"operator\":\"=\",\"value\":\"$ln_number\"}}", "status": "ACTIVE" } ``` ### Step 3b (Python) Python SDK equivalent: Creates the knowledge query using the IndyKite Python client library. **create_knowledge_query.py** ```python import http.client conn = http.client.HTTPSConnection("eu.api.indykite.com") payload = "{"description": "", "display_name": "", "name": "", "policy_id": "", "project_id": "", "query": "", "status": "ACTIVE"}" headers = { 'Content-Type': "application/json", 'Authorization': "YOUR_SECRET_TOKEN" } conn.request("POST", "/configs/v1/knowledge-queries", payload, headers) res = conn.getresponse() data = res.read() ``` ### Step 3c GET request to verify the query was created. Returns the full query configuration including system-generated ID. **GET https://eu.api.indykite.com/configs/v1/knowledge-queries/{query_id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 3c (Python) Python SDK equivalent: Reads the query configuration to verify creation. **read_knowledge_query.py** ```python import http.client conn = http.client.HTTPSConnection("eu.api.indykite.com") headers = { 'Authorization': "YOUR_SECRET_TOKEN" } conn.request("GET", "/configs/v1//knowledge-queries/{id}", headers=headers) res = conn.getresponse() data = res.read() print(data.decode("utf-8")) ``` ### Step 4a Execute query request. Provides the query ID and input parameters (email). The system evaluates authorization and returns only permitted results. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "knowledge_query_gid", "input_params": { "ln_number": "AX123456", "person_email": "ryan@yahoo.co.uk" } } ``` ### Step 4a (Python) Python SDK equivalent: Executes the ContX IQ query with the same parameters. **execute_query.py** ```python import http.client conn = http.client.HTTPSConnection("eu.api.indykite.com") payload = "{"id": "knowledge_query_gid", "input_params": {"ln_number": "AX123456","person_email": "ryan@yahoo.co.uk"} }" headers = { 'Content-Type': "application/json", 'Authorization': "YOUR_SECRET_TOKEN" } conn.request("POST", "/contx-iq/v1/execute", payload, headers) res = conn.getresponse() data = res.read() ``` ### Step 4b Expected response. Contains an array of license number strings that the queried person is authorized to access. **response.json** ```json { "data": [ { "nodes": { "ln.property.number": "AX123456" } } ] } ``` ### Step 5a DELETE request to remove the knowledge query. Use the query ID returned from Step 3. **DELETE https://eu.api.indykite.com/configs/v1/knowledge-queries/{query_id}** ```json { "id": "your_knowledge_query_configuration_gid" } ``` ### Step 5a (Python) Python SDK equivalent: Deletes the knowledge query configuration. **delete_knowledge_query.py** ```python import http.client conn = http.client.HTTPSConnection("eu.api.indykite.com) headers = { 'Authorization': "Bearer ..." } conn.request("DELETE", "/configs/v1/knowledge-queries/{id}", headers=headers) res = conn.getresponse() data = res.read() print(data.decode("utf-8")) ``` ### Step 5b DELETE request to remove the authorization policy. Use the policy ID returned from Step 2. **DELETE https://eu.api.indykite.com/configs/v1/authorization-policies/{policy_id}** ```json { "id": "your_policy_configuration_gid" } ``` ### Step 5b (Python) Python SDK equivalent: Deletes the authorization policy configuration. **delete_policy.py** ```python import http.client conn = http.client.HTTPSConnection("eu.api.indykite.com") headers = { 'Authorization': "Bearer ...", 'Content-Type': "application/json" } conn.request("DELETE", "/configs/v1/authorization-policies/{id}", headers=headers) res = conn.getresponse() data = res.read() ``` ## Common Errors ### 401: UNAUTHENTICATED **Solution:** Check credentials: ServiceAccount Bearer token for config APIs, X-IK-ClientKey for execution ### 404: NOT_FOUND **Solution:** Verify the knowledge query ID exists and belongs to your project ### 400: INVALID_ARGUMENT **Solution:** Check that all required input parameters are provided in the execution request --- Source: https://developer.indykite.com/resources/ciq-basic --- # Token Introspect: Bind Bearer Token Claims to a Person Subject for CIQ Queries > End-to-end example showing how the Token Introspect configuration, the data ingested via the Capture pipeline, and a CIQ policy lock together. The bearer token's sub claim selects the Person subject; the token's email claim cross-references the captured Person.property.email; the CIQ query returns only that user's profile. **Category:** Token Introspect **API:** Token Introspect **Tags:** Token Introspect, ContX IQ, Person Subject, JWT, Identity Binding, Claims Mapping, $token.sub, Capture Pipeline **Last Updated:** 2026-05-19 **OpenAPI Endpoints:** /capture/v1/nodes, /configs/v1/token-introspects, /configs/v1/authorization-policies, /configs/v1/knowledge-queries, /contx-iq/v1/execute **Related Guides:** /guides/guide-token-introspect, /guides/guide-contx-iq, /guides/guide-environment ## Summary Three pieces line up to authorize a bearer-token request and return that user's own data: 1. Capture pipeline ingests a Person node with the external_id and properties your application controls (here, external_id = "alice", property email = "alice@email.com", property name = "Alice Smith"). 2. Token Introspect configuration declares the issuer + audience that produces the JWT, plus claims_mapping that says which claims to project onto the Token node. The sub_claim setting names the JWT claim used as the Token node's external_id. 3. CIQ policy says subject is Person, with a filter subject.external_id = $token.sub. At /contx-iq/v1/execute time, Token Introspect resolves the bearer token, $token.sub is set to "alice", the policy matches Person(alice), and the Knowledge Query returns alice's profile. This is the simplest end-to-end pattern for "let an authenticated user query their own data." The same binding (token claim <-> captured property) generalizes to any claim you choose. ## Use Case Scenario: A web app issues an OIDC bearer token after the user signs in. The app calls IndyKite to fetch the signed-in user's profile from the IKG without telling IndyKite who the user is - the bearer token does that. What flows through the system: Token payload (sub claim is the IdP's user identifier): { "iss": "https://your-idp.example.com/", "sub": "alice", "email": "alice@email.com", "aud": ["client_id-of-your-app"], ... } Captured Person node (created by your ingest pipeline, ahead of time): { "external_id": "alice", "type": "Person", "properties": [ { "type": "email", "value": "alice@email.com" }, { "type": "name", "value": "Alice Smith" } ] } CIQ policy filter: subject.external_id = $token.sub The 'alice' string appears in three places: - as the JWT 'sub' claim - as the Person node's external_id - via the policy filter, which couples them. This is the contract. If you choose to bind by email instead, set the filter to subject.property.email = $token.email and ensure the Capture pipeline writes the email property to match. ## Requirements Prerequisites: - ServiceAccount credentials: For creating the Token Introspect configuration, the policy, and the Knowledge Query (Bearer token in Authorization header). - AppAgent credentials: For ingesting Person nodes and for executing the CIQ query (X-IK-ClientKey header). - External IdP: an OIDC provider (Auth0, Okta, Keycloak, your own) capable of issuing JWTs that include at minimum 'iss', 'aud', and 'sub' claims. - A JWT for the test user. The sub claim must equal the Person.external_id you ingest. Required API access: - POST /capture/v1/nodes (ingest Person) - POST /configs/v1/token-introspects (Token Introspect config - also possible via Terraform) - POST /configs/v1/authorization-policies (CIQ policy) - POST /configs/v1/knowledge-queries (CIQ Knowledge Query) - POST /contx-iq/v1/execute (run the query with the bearer token) ## Steps Step 1: Capture the Person Node - Authentication: AppAgent credential (X-IK-ClientKey). - Action: POST a Person node whose external_id is the value your IdP will put in the JWT sub claim ("alice"). Include the email property and any other identity attributes you want CIQ to be able to return. - Result: Person(alice) is in the IKG, ready to be selected as the CIQ subject at execute time. Step 2: Create the Token Introspect Configuration - Authentication: ServiceAccount credential. Recommended: Terraform. - Action: Configure jwt_matcher.issuer + jwt_matcher.audience to match the JWTs your IdP issues. Set sub_claim = "sub" (binds JWT.sub to the Token node's external_id). Use claims_mapping to project additional claims (e.g., email) onto the Token node so they are available as $token. in CIQ. - Result: IndyKite knows how to validate incoming bearer tokens for this issuer/audience and what to expose to CIQ. Step 3: Create the CIQ Policy - Authentication: ServiceAccount credential. - Action: POST a CIQ policy whose condition filter is subject.external_id = $token.sub. Allow reads on subject and the subject properties you want callers to see. - Result: Policy ID - references the user-identity binding. Step 4: Create the Knowledge Query - Authentication: ServiceAccount credential. - Action: POST a Knowledge Query that returns subject.external_id, subject.property.email, and subject.property.name. No input_params needed - the binding comes from the token, not from the request body. - Result: Knowledge Query ID. Step 5: Execute as the Authenticated User - Authentication: AppAgent credential in X-IK-ClientKey + the user's bearer token in Authorization: Bearer . - Action: POST /contx-iq/v1/execute with the Knowledge Query id and an empty input_params object. - Result: A single record describing Alice. If you call the same endpoint with a different user's bearer token, the same query returns that other user's record. The query body is identical; the token decides the answer. ## Code Examples ### Step 1 Capture the Person node whose external_id will match the JWT sub claim. The Capture pipeline is where the link between identity-provider claims and graph data is established - the external_id you choose here is the contract. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "alice", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "alice@email.com" }, { "type": "given_name", "value": "Alice" }, { "type": "last_name", "value": "Smith" } ] }, { "external_id": "knightrider", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "knightrider@demo.com" }, { "type": "name", "value": "Michael Knight" } ] }, { "external_id": "satchmo", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "satchmo@demo.com" }, { "type": "name", "value": "Louis Armstrong" } ] }, { "external_id": "karel", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "karel@demo.com" }, { "type": "name", "value": "Karel Plihal" } ] }, { "external_id": "kitt", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "pontiac" }, { "type": "model", "value": "Firebird" } ] }, { "external_id": "cadillacv16", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "Cadillac" }, { "type": "model", "value": "V-16" } ] }, { "external_id": "harmonika", "type": "Bus", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "Ikarus" }, { "type": "model", "value": "280" } ] }, { "external_id": "listek", "type": "Ticket", "is_identity": false }, { "external_id": "airbook-xyz", "type": "Laptop", "is_identity": false } ] } ``` ### Step 2 (input) An OIDC access token your IdP issues to Alice after sign-in. Note that sub = "alice" and email = "alice@email.com" - both line up with the captured Person node above. **token.json (JWT payload, decoded)** ```json { "iss": "https://your-idp.example.com/", "sub": "alice", "email": "alice@email.com", "aud": [ "client_id-of-your-app" ], "iat": 1749319876, "exp": 1749406276, "scope": "openid profile email" } ``` ### Step 2 Token Introspect configuration. Two settings carry the entire identity contract: - sub_claim = "sub": the JWT claim used as the Token node's external_id. Combined with the CIQ filter subject.external_id = $token.sub, this is what binds the bearer token to a specific Person node. - claims_mapping: each entry copies a JWT claim onto the Token node as a property. Mapped claims also become available as $token. in CIQ filters, so you can author policies like subject.property.email = $token.email if you prefer to bind by a different field. **token_introspect.tf** ```terraform resource "indykite_token_introspect" "person_subject" { name = "person-subject-introspect" display_name = "Person subject - introspect" description = "Validates bearer tokens for Person subjects in CIQ queries." location = "ProjectGID" jwt_matcher { issuer = "https://your-idp.example.com/" audience = "client_id-of-your-app" } offline_validation {} # External-id-of-token-node is taken from the 'sub' claim by default. # Setting sub_claim explicitly is the most common way to bind a JWT to an # existing Person node: the value of this claim becomes the Token node's # external_id, and the policy filter (subject.external_id = $token.sub) # then resolves the subject to the matching Person. sub_claim = "sub" # claims_mapping copies token claims onto the Token node as properties. # Below, the 'email' claim becomes a property of type 'email' on the Token # node, which lets you cross-reference the authenticated identity in # Knowledge Queries (e.g., subject.property.email = $token.email). claims_mapping = { "email" = "email" } ikg_node_type = "Token" perform_upsert = true } ``` ### Step 3 CIQ Policy with the simplest possible Person subject: - subject.type = Person. - condition.cypher matches every Person and the filter narrows it to the one whose external_id equals $token.sub. - allowed_reads exposes subject and two of its properties so the Knowledge Query can return them. **policy.json** ```json { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "Person" }, "condition": { "cypher": "MATCH (subject:Person)", "filter": [ { "operator": "=", "attribute": "subject.external_id", "value": "$token.sub" } ] }, "allowed_reads": { "nodes": [ "subject", "subject.property.email", "subject.property.name" ] } } ``` Request to create the policy. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "CIQ policy authorizing the Person subject whose external_id matches the bearer token's sub claim. The binding relies on Token Introspect resolving the JWT and exposing sub via $token.sub.", "display_name": "policy - person from token", "name": "policy-person-from-token", "policy": "{\"meta\":{\"policy_version\":\"1.0-ciq\"},\"subject\":{\"type\":\"Person\"},\"condition\":{\"cypher\":\"MATCH (subject:Person)\",\"filter\":[{\"operator\":\"=\",\"attribute\":\"subject.external_id\",\"value\":\"$token.sub\"}]},\"allowed_reads\":{\"nodes\":[\"subject\",\"subject.property.email\",\"subject.property.name\"]}}", "status": "ACTIVE", "tags": [] } ``` ### Step 4 Knowledge Query - returns the authenticated user's own external_id, email, and name. No input params: the policy already selects the subject from the token. **knowledge_query.json** ```json { "nodes": [ "subject.external_id", "subject.property.email", "subject.property.name" ] } ``` Request to create the Knowledge Query. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "Return the authenticated Person's own profile. The subject is selected by matching subject.external_id against $token.sub (resolved by Token Introspect).", "display_name": "knowledge query - my profile", "name": "kq-my-profile", "policy_id": "your_policy_gid", "query": "{\"nodes\":[\"subject.external_id\",\"subject.property.email\",\"subject.property.name\"]}", "status": "ACTIVE" } ``` ### Step 5 Execute the query as Alice. Authentication = AppAgent credential in X-IK-ClientKey + Alice's bearer token in Authorization: Bearer . input_params is empty because the identity binding comes from the JWT. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "your_query_gid_or_name", "input_params": {} } ``` Response - exactly Alice's record. Sending the same request with Bob's bearer token would return Bob's record instead. The query body is unchanged; the token decides. **response.json** ```json { "data": [ { "nodes": { "subject.external_id": "alice", "subject.property.email": "alice@email.com", "subject.property.name": "Alice Smith" } } ] } ``` ## Common Errors ### 401: invalid bearer token **Solution:** Confirm jwt_matcher.issuer and jwt_matcher.audience in the Token Introspect config exactly match the iss and aud claims in the JWT. A mismatch on either silently rejects the token. ### 200: empty data array **Solution:** The query ran but no Person matched. Most common cause: the JWT sub claim does not equal the Person.external_id you captured. Print the introspected token to confirm sub, and confirm the captured Person.external_id matches it byte-for-byte. ### 400: missing token **Solution:** When the policy subject is not _Application, the Authorization: Bearer header is required in addition to X-IK-ClientKey. Sending only X-IK-ClientKey fails validation. --- Source: https://developer.indykite.com/resources/cred-1 --- # Set Up IndyKite Environment: Project, Application, and Credentials > Create the foundational IndyKite environment components required before using any IndyKite product. This includes creating a project, application, application agent, credentials, and token introspect configuration. **Category:** Environment **API:** Environment **Tags:** Environment Setup, Project Creation, Application Agent, Credentials, Token Introspect **Last Updated:** 2026-08-31 **OpenAPI Endpoints:** /configs/v1/projects, /configs/v1/applications, /configs/v1/application-agents, /configs/v1/application-agent-credentials, /configs/v1/token-introspects **Related Guides:** /guides/guide-environment, /guides/guide-credentials, /guides/guide-sandbox ## Summary This guide walks through the complete environment setup process: 1. Create a Project - the top-level container for your IndyKite resources 2. Create an Application - represents your software that will interact with IndyKite 3. Create an Application Agent - the identity used for API authentication 4. Generate Application Agent Credentials - the API key for authentication 5. Configure Token Introspect - enables token validation for your application Complete these steps before using any other IndyKite feature (IKG, ContX IQ, KBAC, etc.). ## Use Case Scenario: You are setting up a new application that needs to: - Store identity and resource data in the IndyKite Knowledge Graph (IKG) - Use ContX IQ for authorized data queries - Use KBAC for access control decisions Before any of these features work, you must create the environment hierarchy: Organization (already exists) -> Project -> Application -> Application Agent -> Credentials ## Requirements Prerequisites: - ServiceAccount credentials: Created in IndyKite Hub or via REST API for your Organization - Organization ID: Your IndyKite organization identifier Required API access: - POST /configs/v1/projects (create project) - POST /configs/v1/applications (create application) - POST /configs/v1/application-agents (create agent) - POST /configs/v1/application-agent-credentials (generate credentials) - POST /configs/v1/token-introspects (configure token validation) ## Steps Step 1: Create a Project - Authentication: ServiceAccount credential as Bearer token in Authorization header - Action: POST to /configs/v1/projects with project name and configuration - Input: Organization ID, project display name, optional BYODB (Bring Your Own Database) configuration - Result: Project ID returned for use in subsequent steps Step 2: Create an Application - Authentication: ServiceAccount credential as Bearer token - Action: POST to /configs/v1/applications - Input: Project ID from Step 1, application display name - Result: Application ID returned Step 3: Create an Application Agent - Authentication: ServiceAccount credential as Bearer token - Action: POST to /configs/v1/application-agents - Input: Application ID from Step 2, agent display name - Result: Application Agent ID returned Step 4: Generate Application Agent Credentials - Authentication: ServiceAccount credential as Bearer token - Action: POST to /configs/v1/application-agent-credentials - Input: Application Agent ID from Step 3 - Result: Credential JSON containing the API key (X-IK-ClientKey value) - Important: Save these credentials securely - they cannot be retrieved again Step 5: Download Credentials - Action: Save the credential JSON from Step 4 to a secure location - Use: This credential is used as the X-IK-ClientKey header for data ingestion and query execution Step 6: Configure Token Introspect - Authentication: ServiceAccount credential as Bearer token - Action: POST to /configs/v1/token-introspects - Input: Application Agent ID, token validation settings - Result: Token introspect configuration ID returned ## Code Examples ### Step 1 POST request to create a new project. The project is the container for applications and their data. Optionally configure BYODB (Bring Your Own Database) for custom storage. **POST https://eu.api.indykite.com/configs/v1/projects** ```json { "db_connection": { "password": "example-password", "url": "neo4j+s://xxxxxxxx.databases.neo4j.io", "username": "neo4j" }, "description": "Project description", "display_name": "Project name", "ikg_size": "2GB", "name": "project-name", "organization_id": "gid-of-organization", "region": "us-east1" } ``` ### Step 2 POST request to create an application within the project. The application represents your software system that will use IndyKite services. **POST https://eu.api.indykite.com/configs/v1/applications** ```json { "description": "Application description", "display_name": "Application name", "name": "app-name", "project_id": "gid-of-project" } ``` ### Step 3 POST request to create an application agent. The agent is the authenticated identity that your application uses to call IndyKite APIs. **POST https://eu.api.indykite.com/configs/v1/application-agents** ```json { "api_permissions": [ "Authorization", "Capture", "ContXIQ", "EntityMatching" ], "application_id": "gid-of-application", "description": "App Agent description", "display_name": "App Agent name", "name": "app-agent-name" } ``` ### Step 4 POST request to generate credentials for the application agent. The response contains the API key to use in the X-IK-ClientKey header. Save this securely - it cannot be retrieved again. **POST https://eu.api.indykite.com/configs/v1/application-agent-credentials** ```json { "application_agent_id": "gid-of-app-agent", "display_name": "AppAgent Credentials name", "expire_time": "2026-12-31T12:34:56-01:00" } ``` ### Step 6 POST request to create a token introspect configuration. This enables your application to validate tokens and extract identity information. **POST https://eu.api.indykite.com/configs/v1/token-introspects** ```json { "claims_mapping": { "email": { "selector": "email" }, "name": { "selector": "full_name" } }, "description": "Token introspect description", "display_name": "Token introspect name", "ikg_node_type": "Person", "jwt_matcher": { "audience": "audience-id", "issuer": "https://example.com" }, "name": "rest-token-introspect", "online_validation": { "cache_ttl": 600 }, "perform_upsert": true, "project_id": "gid-of-project" } ``` ## Common Errors ### 401: UNAUTHENTICATED **Solution:** Verify ServiceAccount credentials are valid and included as Bearer token in Authorization header ### 403: PERMISSION_DENIED **Solution:** Ensure ServiceAccount has sufficient permissions at the Organization level ### 404: NOT_FOUND **Solution:** Check that the parent resource ID (organization, project, or application) exists --- Source: https://developer.indykite.com/resources/environment-1 --- # Outbound Events: Configure Kafka Event Streaming > Set up real-time event streaming from IndyKite to Kafka (Confluent). Receive notifications when graph data changes, configurations are modified, or specific actions occur. **Category:** Outbound Events **API:** Outbound Events **Tags:** Outbound Events, Kafka, Confluent, Event Streaming, Webhooks, Real-time **Last Updated:** 2026-03-20 **OpenAPI Endpoints:** /configs/v1/event-sinks **Related Guides:** /guides/guide-outbound-events, /guides/guide-sandbox ## Summary This example configures Outbound Events with Kafka as the message broker: What are Outbound Events? Real-time notifications sent to external systems when changes occur in IndyKite. Supported events: - Configuration changes (policies, queries created/updated/deleted) - Graph data changes (nodes/relationships created/updated/deleted) - Authorization events (evaluations, access decisions) Architecture: IndyKite -> Event Sink Configuration -> Kafka Topic -> Your Consumer Application One Event Sink per project. Multiple routes can direct different event types to different topics. ## Use Case Scenario: You want to receive notifications whenever configuration changes are made in your project. Event flow: 1. Admin creates/updates/deletes a policy 2. IndyKite detects the configuration change 3. Event Sink sends message to Kafka topic 4. Your consumer application receives the event 5. Application can: log, alert, sync, trigger workflows Example events: - Policy created: {type: "config.created", resource: "authorization-policy", id: "pol-123"} - Node updated: {type: "capture.updated", node_type: "Person", id: "person-456"} - Authorization: {type: "access.evaluated", decision: true, subject: "alice"} Terraform registry for supported filters: https://registry.terraform.io/providers/indykite/indykite/latest/docs/resources/event_sink ## Requirements Prerequisites: - ServiceAccount credentials: For creating Event Sink configuration (Bearer token) - Confluent Cloud account: With API key and cluster access - Kafka topic: Created in your Confluent environment Confluent requirements: - Cluster bootstrap URL - API Key and Secret for authentication - Topic name for event delivery ## Steps Step 1: Create Kafka Topic on Confluent. 2. Create an Outbound Events configuration and a KBAC policy configuration. 3. Check that messages are received in the topic each time a CRUD action is executed on any configuration node (including any read action on Outbound Events). ## Code Examples ### Step 2 Create an EventSink configuration. **POST https://eu.api.indykite.com/configs/v1/event-sinks** ```json { "project_id": "your_project_gid", "description": "description of eventsink", "display_name": "eventsink name", "name": "eventsink-name", "providers": { "provider-with-kafka": { "include_cdc_events": false, "kafka": { "brokers": [ "http://your-destination:9092" ], "disable_tls": false, "tls_skip_verify": false, "topic": "topic_signal", "username": "api_key", "password": "api_key_secret" } } }, "routes": [ { "provider_id": "provider-with-kafka", "event_type_key_values_filter": { "event_type": "indykite.audit.config.*" }, "stop_processing": true, "display_name": "Configuration Audit Events" } ] } ``` Create a simple Policy which designates the Person nodes who can drive a resource Car. **policy.json** ```json { "meta": { "policy_version": "2.0-kbac" }, "subject": { "type": "Person" }, "actions": [ "CAN_DRIVE" ], "resource": { "type": "Car" }, "condition": { "cypher": "MATCH (subject:Person)-[:DRIVES]->(resource:Car)" } } ``` ## Common Errors ### 401: UNAUTHENTICATED **Solution:** Verify ServiceAccount credentials are valid ### 400: INVALID_ARGUMENT **Solution:** Check event sink configuration format and destination URL --- Source: https://developer.indykite.com/resources/event-1 --- # Outbound Events: Stream Graph Data Changes to Kafka > Configure event streaming for Knowledge Graph changes. Receive real-time notifications when specific node types are created, updated, or deleted. Filter by node labels and properties. **Category:** Outbound Events **API:** Outbound Events **Tags:** Outbound Events, Kafka, Capture Events, Node Changes, Graph Streaming **Last Updated:** 2026-07-15 **OpenAPI Endpoints:** /configs/v1/event-sinks **Related Guides:** /guides/guide-outbound-events, /guides/guide-sandbox ## Summary This example streams Capture audit events (node upserts and deletes) to Kafka: Event types covered: - indykite.audit.capture.upsert.node (node created/updated) - indykite.audit.capture.delete.node (node deleted) - indykite.audit.capture.* (all capture events) Note: these are Capture audit events, not CDC. CDC (Change Data Capture) events carry the full before/after state of a change, use the indykite.audit.cdc.* event types, and are enabled per provider with include_cdc_events - see the Outbound Events guide. Filtering capability: Filter events by node label and property values to receive only relevant changes. Example: Only stream events for Car nodes where manufacturer="pontiac" ## Use Case Scenario: Stream car inventory changes to an external analytics system, but only for Pontiac vehicles. Event filter configuration: - Event type: indykite.audit.capture.* - Label filter: "Car" - Property filter: manufacturer = "pontiac" Events generated: 1. Ingest Car(manufacturer:"pontiac") -> Event sent 2. Ingest Car(manufacturer:"ford") -> No event (filtered out) 3. Update Car(manufacturer:"pontiac") -> Event sent 4. Delete Car(manufacturer:"pontiac") -> Event sent This enables selective streaming without overwhelming your analytics pipeline. ## Requirements Prerequisites: - ServiceAccount credentials: For configuration (Bearer token) - AppAgent credentials: For data ingestion (X-IK-ClientKey) - Confluent Cloud: API key and Kafka topic ready Terraform documentation for filter syntax: https://registry.terraform.io/providers/indykite/indykite/latest/docs/resources/event_sink ## Steps Step 1: Create Kafka Topic - Action: Set up topic on Confluent Cloud for receiving car events Step 2: Create Event Sink Configuration - Authentication: ServiceAccount credential (Bearer token) - Action: POST Event Sink with filters: - eventType: "indykite.audit.capture.*" - nodeLabel: "Car" - propertyFilter: {manufacturer: "pontiac"} - Result: Event Sink active Step 3: Ingest Initial Car Nodes - Authentication: AppAgent credential (X-IK-ClientKey) - Action: POST Car nodes including some with manufacturer="pontiac" - Result: Events sent for matching nodes only Step 4: Ingest Additional Matching Nodes - Action: POST more Car(manufacturer:"pontiac") nodes - Result: Each matching node generates an event Step 5: Verify Event Delivery - Check Kafka topic for received events - Verify non-matching nodes (other manufacturers) did not generate events ## Code Examples ### Step 2 Create an EventSink configuration. **POST https://eu.api.indykite.com/configs/v1/event-sinks** ```json { "project_id": "your_project_gid", "description": "description of eventsink", "display_name": "eventsink name", "name": "eventsink-name", "providers": { "provider-with-kafka": { "include_cdc_events": false, "kafka": { "brokers": [ "http://your-destination:9092" ], "disable_tls": false, "tls_skip_verify": false, "topic": "topic_signal", "username": "api_key", "password": "api_key_secret" } } }, "routes": [ { "provider_id": "provider-with-kafka", "event_type_key_values_filter": { "context_key_value": [ { "key": "manufacturer", "value": "pontiac" }, { "key": "captureLabel", "value": "Car" } ], "event_type": "indykite.audit.capture.upsert.node" }, "stop_processing": true, "display_name": "Configuration Audit Events" } ] } ``` ### Step 3 Capture the nodes needed for this use case. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "alice", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "alice@email.com" }, { "type": "given_name", "value": "Alice" }, { "type": "last_name", "value": "Smith" } ] }, { "external_id": "knightrider", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "knightrider@demo.com" }, { "type": "name", "value": "Michael Knight" } ] }, { "external_id": "satchmo", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "satchmo@demo.com" }, { "type": "name", "value": "Louis Armstrong" } ] }, { "external_id": "karel", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "karel@demo.com" }, { "type": "name", "value": "Karel Plihal" } ] }, { "external_id": "kitt", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "pontiac" }, { "type": "model", "value": "Firebird" } ] }, { "external_id": "cadillacv16", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "Cadillac" }, { "type": "model", "value": "V-16" } ] }, { "external_id": "harmonika", "type": "Bus", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "Ikarus" }, { "type": "model", "value": "280" } ] }, { "external_id": "listek", "type": "Ticket", "is_identity": false }, { "external_id": "airbook-xyz", "type": "Laptop", "is_identity": false } ] } ``` ### Step 4 Capture the nodes needed for this use case. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "kitten", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "pontiac" }, { "type": "model", "value": "Bonneville" } ] }, { "external_id": "kitty", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "pontiac" }, { "type": "model", "value": "Catalina" } ] } ] } ``` ## Common Errors ### 401: UNAUTHENTICATED **Solution:** Verify ServiceAccount credentials are valid ### 400: INVALID_ARGUMENT **Solution:** Check event sink configuration format and destination URL --- Source: https://developer.indykite.com/resources/event-2 --- # Outbound Events: Stream to Azure Event Grid > Configure event streaming to Azure Event Grid for serverless event processing. Trigger Azure Functions, Logic Apps, or other Azure services when graph data changes. **Category:** Outbound Events **API:** Outbound Events **Tags:** Outbound Events, Azure Event Grid, Azure, Serverless, Event-Driven **Last Updated:** 2026-07-15 **OpenAPI Endpoints:** /configs/v1/event-sinks **Related Guides:** /guides/guide-outbound-events, /guides/guide-sandbox ## Summary This example streams graph events to Azure Event Grid: Azure Event Grid benefits: - Native Azure integration (Functions, Logic Apps, etc.) - Serverless event routing - High availability and scalability - Event filtering at the grid level Event type in this example: indykite.audit.capture.upsert.node (node create/update only) Use with Azure: - Azure Functions: Trigger code on events - Logic Apps: Workflow automation - Event Hubs: Stream analytics - Custom webhooks: Any HTTP endpoint ## Use Case Scenario: Trigger an Azure Function whenever a Pontiac car is added to inventory. Configuration: - Event type: indykite.audit.capture.upsert.node (upsert only, not delete) - Filter: Car nodes where manufacturer="pontiac" - Destination: Azure Event Grid topic Azure Function workflow: 1. Car(manufacturer:"pontiac") ingested in IndyKite 2. Event sent to Azure Event Grid 3. Azure Function triggered 4. Function processes event (e.g., sends notification, updates dashboard) This enables reactive, serverless architectures with IndyKite as the data source. ## Requirements Prerequisites: - ServiceAccount credentials: For configuration (Bearer token) - AppAgent credentials: For data ingestion (X-IK-ClientKey) - Azure Event Grid topic: With endpoint URL and access key Azure setup: - Create Event Grid topic in Azure portal - Note the topic endpoint URL - Generate access key for authentication ## Steps Step 1: Set Up Azure Event Grid Topic - Action: Create Event Grid topic in Azure portal - Note: Topic endpoint URL and access key needed Step 2: Create Event Sink Configuration - Authentication: ServiceAccount credential (Bearer token) - Action: POST Event Sink with: - Provider: Azure Event Grid - Event type: indykite.audit.capture.upsert.node - Filters: Car label, manufacturer="pontiac" - Endpoint: Your Event Grid topic URL - Result: Event Sink active Step 3: Ingest Matching Nodes - Authentication: AppAgent credential (X-IK-ClientKey) - Action: POST Car nodes with manufacturer="pontiac" - Result: Events delivered to Azure Event Grid Step 4-5: Add More Nodes and Verify - Add additional matching nodes - Check Azure portal for received events - Verify non-matching nodes don't trigger events ## Code Examples ### Step 2 Create an EventSink configuration. **POST https://eu.api.indykite.com/configs/v1/event-sinks** ```json { "project_id": "your_project_gid", "description": "description of eventsink", "display_name": "eventsink name", "name": "eventsink-name", "providers": { "provider-with-azure-event-grid": { "include_cdc_events": false, "azure_event_grid": { "topicEndpoint": "https://ik-test.eventgrid.azure.net/api/events", "accessKey": "secret-access-key" } } }, "routes": [ { "provider_id": "provider-with-azure-event-grid", "event_type_key_values_filter": { "context_key_value": [ { "key": "manufacturer", "value": "pontiac" }, { "key": "captureLabel", "value": "Car" } ], "event_type": "indykite.audit.capture.upsert.node" }, "stop_processing": true, "display_name": "Configuration Audit Events" } ] } ``` ### Step 3 Capture the nodes needed for this use case. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "alice", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "alice@email.com" }, { "type": "given_name", "value": "Alice" }, { "type": "last_name", "value": "Smith" } ] }, { "external_id": "knightrider", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "knightrider@demo.com" }, { "type": "name", "value": "Michael Knight" } ] }, { "external_id": "satchmo", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "satchmo@demo.com" }, { "type": "name", "value": "Louis Armstrong" } ] }, { "external_id": "karel", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "karel@demo.com" }, { "type": "name", "value": "Karel Plihal" } ] }, { "external_id": "kitt", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "pontiac" }, { "type": "model", "value": "Firebird" } ] }, { "external_id": "cadillacv16", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "Cadillac" }, { "type": "model", "value": "V-16" } ] }, { "external_id": "harmonika", "type": "Bus", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "Ikarus" }, { "type": "model", "value": "280" } ] }, { "external_id": "listek", "type": "Ticket", "is_identity": false }, { "external_id": "airbook-xyz", "type": "Laptop", "is_identity": false } ] } ``` ### Step 4 Capture the nodes needed for this use case. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "kitten", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "pontiac" }, { "type": "model", "value": "Bonneville" } ] }, { "external_id": "kitty", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "pontiac" }, { "type": "model", "value": "Catalina" } ] } ] } ``` ## Common Errors ### 401: UNAUTHENTICATED **Solution:** Verify ServiceAccount credentials are valid ### 400: INVALID_ARGUMENT **Solution:** Check event sink configuration format and destination URL --- Source: https://developer.indykite.com/resources/event-3 --- # Outbound Events: Stream to Azure Service Bus > Configure event streaming to Azure Service Bus for enterprise messaging patterns. Enable reliable message delivery with queues and topics for decoupled, scalable architectures. **Category:** Outbound Events **API:** Outbound Events **Tags:** Outbound Events, Azure Service Bus, Azure, Enterprise Messaging, Message Queue **Last Updated:** 2026-03-20 **OpenAPI Endpoints:** /configs/v1/event-sinks **Related Guides:** /guides/guide-outbound-events, /guides/guide-sandbox ## Summary This example streams graph events to Azure Service Bus: Azure Service Bus benefits: - Enterprise-grade messaging with guaranteed delivery - Topics with multiple subscriptions (fan-out pattern) - Dead-letter queues for failed messages - Message sessions and ordering - Integration with Azure services Difference from Event Grid: - Service Bus: Reliable message queuing, enterprise patterns - Event Grid: Lightweight event routing, serverless triggers ## Use Case Scenario: Send inventory updates to multiple downstream systems reliably. Service Bus architecture: IndyKite -> Event Sink -> Service Bus Topic ├-> Subscription 1 (Inventory System) ├-> Subscription 2 (Analytics) └-> Subscription 3 (Audit Log) Event configuration: - Event type: indykite.audit.capture.* (all CRUD operations) - Filter: Car nodes where manufacturer="pontiac" Benefits: - Each subscription processes events independently - Failed processing goes to dead-letter queue - Guaranteed at-least-once delivery ## Requirements Prerequisites: - ServiceAccount credentials: For configuration (Bearer token) - AppAgent credentials: For data ingestion (X-IK-ClientKey) - Azure Service Bus namespace with: - Topic created - Topic subscription configured - Connection string available ## Steps Step 1: Set Up Azure Service Bus - Action: Create Service Bus namespace, topic, and subscription - Note: Connection string needed for authentication Step 2: Create Event Sink Configuration - Authentication: ServiceAccount credential (Bearer token) - Action: POST Event Sink with: - Provider: Azure Service Bus - Event type: indykite.audit.capture.* - Filters: Car label, manufacturer="pontiac" - Connection: Service Bus namespace and topic - Result: Event Sink active Step 3: Ingest Matching Nodes - Authentication: AppAgent credential (X-IK-ClientKey) - Action: POST Car nodes with manufacturer="pontiac" - Result: Messages delivered to Service Bus topic Step 4-6: Verify Message Delivery - Add and delete matching nodes - Check Service Bus for received messages - Verify non-matching operations don't generate messages ## Code Examples ### Step 2 Create an EventSink configuration. **POST https://eu.api.indykite.com/configs/v1/event-sinks** ```json { "project_id": "your_project_gid", "description": "description of eventsink", "display_name": "eventsink name", "name": "eventsink-name", "providers": { "provider-with-azure-service-bus": { "include_cdc_events": false, "azure_service_bus": { "queueOrTopicName": "test-queue", "connectionString": "Endpoint=sb://ik-test.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=a...." } } }, "routes": [ { "provider_id": "provider-with-azure-service-bus", "event_type_key_values_filter": { "context_key_value": [ { "key": "manufacturer", "value": "pontiac" }, { "key": "captureLabel", "value": "Car" } ], "event_type": "indykite.audit.capture.upsert.node" }, "stop_processing": true, "display_name": "Configuration Audit Events" } ] } ``` ### Step 3 Capture the nodes needed for this use case. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "alice", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "alice@email.com" }, { "type": "given_name", "value": "Alice" }, { "type": "last_name", "value": "Smith" } ] }, { "external_id": "knightrider", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "knightrider@demo.com" }, { "type": "name", "value": "Michael Knight" } ] }, { "external_id": "satchmo", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "satchmo@demo.com" }, { "type": "name", "value": "Louis Armstrong" } ] }, { "external_id": "karel", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "karel@demo.com" }, { "type": "name", "value": "Karel Plihal" } ] }, { "external_id": "kitt", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "pontiac" }, { "type": "model", "value": "Firebird" } ] }, { "external_id": "cadillacv16", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "Cadillac" }, { "type": "model", "value": "V-16" } ] }, { "external_id": "harmonika", "type": "Bus", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "Ikarus" }, { "type": "model", "value": "280" } ] }, { "external_id": "listek", "type": "Ticket", "is_identity": false }, { "external_id": "airbook-xyz", "type": "Laptop", "is_identity": false } ] } ``` ### Step 4 Capture the nodes needed for this use case. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "kitten", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "pontiac" }, { "type": "model", "value": "Bonneville" } ] }, { "external_id": "kitty", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "pontiac" }, { "type": "model", "value": "Catalina" } ] } ] } ``` ## Common Errors ### 401: UNAUTHENTICATED **Solution:** Verify ServiceAccount credentials are valid ### 400: INVALID_ARGUMENT **Solution:** Check event sink configuration format and destination URL --- Source: https://developer.indykite.com/resources/event-4 --- # External Data Resolver: Refresh a Node Property at Write Time from an External API > Use a CIQ upsert with external_value to write a property whose value comes from an External Data Resolver invoked at execute time. The IKG never stores stale prices: the upsert calls the pricing service, writes the result, and the same /contx-iq/v1/execute call returns the freshly-written value. **Category:** External Data Resolver **API:** External Data Resolver **Tags:** External Data Resolver, ContX IQ Policy, ContX IQ Query, ContX IQ Execution, upsert_nodes, external_value, Write-Time Lookup **Last Updated:** 2026-05-20 **OpenAPI Endpoints:** /configs/v1/external-data-resolvers, /capture/v1/nodes, /capture/v1/relationships, /configs/v1/authorization-policies, /configs/v1/knowledge-queries, /contx-iq/v1/execute **Related Guides:** /guides/guide-external-data-resolver, /guides/guide-contx-iq ## Summary 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. ## Code Examples ### 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. **POST https://eu.api.indykite.com/configs/v1/external-data-resolvers** ```json { "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. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "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. **POST https://eu.api.indykite.com/capture/v1/relationships/** ```json { "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. **policy.json** ```json { "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. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "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. **knowledge_query.json** ```json { "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. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "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. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "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. **response.json** ```json { "data": [ { "nodes": { "car.external_id": "kitt", "car.property.current_value": 9.99, "car.property.current_value.metadata.source": "pricing-api" } } ] } ``` ## Common Errors ### 400: external_value references missing input param **Solution:** Send value_source in input_params with the exact name of an existing External Data Resolver config in this project. ### 424: External Data Resolver failed **Solution:** Check the External Data Resolver's url, method, and response_selector. The IndyKite runtime treats upstream failures as Failed Dependency. Inspect the resolver's response shape and adjust response_selector to a path that exists. ### 401: UNAUTHENTICATED **Solution:** X-IK-ClientKey must carry a valid AppAgent token; Authorization: Bearer must carry a valid user token when the subject type is Person. --- Source: https://developer.indykite.com/resources/extdata-1 --- # External Data Resolver: Compose Two Resolvers in a Single Knowledge Query > Attach two External Data Resolver configurations to two different properties on the same node - at read time, CIQ invokes both resolvers and returns their results alongside ordinary IKG data, all in one /contx-iq/v1/execute call. **Category:** External Data Resolver **API:** External Data Resolver **Tags:** External Data Resolver, ContX IQ Policy, ContX IQ Query, Multi-Resolver, response_selector, Read-Time Lookup, Catalog Lookup **Last Updated:** 2026-05-20 **OpenAPI Endpoints:** /configs/v1/external-data-resolvers, /capture/v1/nodes, /capture/v1/relationships, /configs/v1/authorization-policies, /configs/v1/knowledge-queries, /contx-iq/v1/execute **Related Guides:** /guides/guide-external-data-resolver, /guides/guide-contx-iq ## Summary 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: }. 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 Scenario: A catalog screen renders two externally-owned fields next to each book, but neither field lives in the IKG (an external service owns the source of truth). Here the demo upstream is https://dummyjson.com/products/1, which returns a JSON object with .brand and .category fields. 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: } 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. ## Code Examples ### 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. **POST https://eu.api.indykite.com/configs/v1/external-data-resolvers** ```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. **POST https://eu.api.indykite.com/configs/v1/external-data-resolvers** ```json { "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. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "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. **POST https://eu.api.indykite.com/capture/v1/relationships/** ```json { "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. **policy.json** ```json { "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. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "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. **knowledge_query.json** ```json { "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. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "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. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "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. **response.json** ```json { "data": [ { "nodes": { "book.property.title": "Adventures Underground", "publisher.property.name": "Wonderland Press", "publisher.property.brand": "Essence", "publisher.property.category": "beauty" } } ] } ``` ## Common Errors ### 424: External Data Resolver failed **Solution:** If one of the two resolvers fails the whole record fails with Failed Dependency. Validate each External Data Resolver independently by reading a node with only that property and confirm response_selector lines up with the upstream response shape. ### 400: property not in allowed_reads **Solution:** External Data Resolver-sourced properties still need to appear in allowed_reads on the policy. Add publisher.property.brand and publisher.property.category there if you see this error. ### 404: external_value references unknown resolver name **Solution:** The string under external_value.name on the captured node must match the External Data Resolver config's name field exactly. Rename or recreate one of them so they line up. --- Source: https://developer.indykite.com/resources/extdata-2 --- # MCP Server: Initialize Sessions and Execute Tools via HTTP > Demonstrates how to interact with the IndyKite MCP (Model Context Protocol) server using HTTP requests, with the session-based MCP protocol (revision 2025-11-25). Covers session initialization, listing resources and tools, and executing authorization queries through MCP tools. For the stateless protocol (revision 2026-07-28), see the companion resource. **Category:** MCP **API:** MCP **Tags:** MCP, Model Context Protocol, ContX IQ, KBAC, authZEN, AI Integration, LLM Tools **Last Updated:** 2026-03-20 **OpenAPI Endpoints:** /configs/v1/mcp-servers, /capture/v1/nodes, /capture/v1/relationships, /configs/v1/authorization-policies, /configs/v1/knowledge-queries **Related Guides:** /guides/guide-mcp ## Summary This example demonstrates the IndyKite MCP Server for AI/LLM integration: What is MCP? Model Context Protocol - a standard for exposing tools and resources to AI models (Claude, GPT, etc.). IndyKite MCP Server provides: 1. Resources - List and read knowledge queries available in your project 2. Tools - Execute authorization operations: - authzen_evaluate: Check if subject can perform action on resource - ciq_execute: Run ContX IQ queries with parameters Protocol used here: Session-based MCP protocol, revision 2025-11-25 - an initialize handshake starts the session, the server returns an Mcp-Session-Id header, and every follow-up request sends it back. (The MCP server also supports the stateless protocol, revision 2026-07-28 - see the companion resource.) Workflow: 1. Initialize session with MCP server 2. List available resources (knowledge queries) 3. List available tools (authzen_evaluate, ciq_execute) 4. Execute tools with parameters This enables AI agents to make authorization decisions using IndyKite. ## Use Case Scenario: An AI assistant needs to check user permissions and retrieve authorized data. MCP Integration flow: 1. AI agent connects to IndyKite MCP Server 2. Initializes session -> receives capabilities and session ID 3. Lists tools -> discovers authzen_evaluate and ciq_execute 4. User asks: "Can I drive the company car?" 5. AI calls authzen_evaluate tool with: - subject: {type: "Person", id: "user-123"} - action: {name: "drive"} - resource: {type: "Car", id: "car-456"} 6. MCP evaluates against KBAC policy 7. Returns: {decision: true} -> AI responds "Yes, you can drive the car" For data retrieval: 1. User asks: "What payment methods do I have?" 2. AI calls ciq_execute tool with knowledge query ID 3. MCP runs ContX IQ query with user's authorization context 4. Returns payment method data 5. AI presents results to user ## Requirements Prerequisites: - ServiceAccount credentials: For creating policies, queries, and the MCP Server configuration (Bearer token) - AppAgent: Referenced by the MCP Server config as the identity the server uses to call IndyKite APIs at runtime (resolved server-side) - User access token: JWT for the subject performing authorization checks - Token Introspect configuration: Required to validate user access tokens (referenced by the MCP Server config) MCP Server endpoint: - URL: https://eu.mcp.indykite.com/mcp/v1/{project_gid} - Protocol: JSON-RPC 2.0 over HTTP POST - session-based MCP protocol, revision 2025-11-25 (initialize handshake + Mcp-Session-Id header on every follow-up request) Required configurations: - MCP Server: Binds the runtime MCP endpoint to an AppAgent + Token Introspect, declares supported OAuth scopes - KBAC Policy: Defines authorization rules (e.g., who can drive cars) - ContX IQ Policy and Query: Defines data access rules and queries ## Steps Step 1: Create the MCP Server configuration - Authentication: ServiceAccount credential (Bearer token) - Action: POST to /configs/v1/mcp-servers - Input: AppAgent ID, Token Introspect ID, project ID, supported OAuth scopes - Result: MCP Server config ID returned; the runtime MCP endpoint will use this binding Step 2: Ingest Graph Data - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: POST nodes (Person, Car) and relationships (DRIVES) - Note: Replace bearer-token-sub with actual user's token subject claim - Result: Graph ready for authorization queries Step 3: Create ContX IQ Policy - Authentication: ServiceAccount credential (Bearer token) - Action: POST policy for data access through MCP - Result: Policy ID returned Step 4: Create ContX IQ Knowledge Query - Authentication: ServiceAccount credential (Bearer token) - Action: POST query with descriptive name and parameters - Important: Include detailed description for AI agent understanding - Result: Query ID available via MCP resources Step 5: Create KBAC Policy - Authentication: ServiceAccount credential (Bearer token) - Action: POST authZEN policy (e.g., Person can DRIVE Car if DRIVES relationship exists) - Result: Policy ID returned, available via authzen_evaluate tool Step 6: Use MCP Server - Initialize session: Send "initialize" JSON-RPC request - List resources: "resources/list" returns available knowledge queries - List tools: "tools/list" returns authzen_evaluate and ciq_execute - Execute authzen_evaluate: Check authorization with subject/action/resource - Execute ciq_execute: Run knowledge query with parameters ## Code Examples ### Step 1 Create the MCP Server configuration. This binds the runtime MCP endpoint (https://.mcp.indykite.com/mcp/v1/) to an AppAgent (the identity the server uses to call IndyKite APIs at runtime, resolved server-side) and a Token Introspect config (used to validate Bearer tokens), and declares the OAuth scopes the server advertises in its .well-known/oauth-protected-resource metadata. Replace gid-of-app-agent and gid-of-token-introspect with the IDs returned in the environment-setup steps. **POST https://eu.api.indykite.com/configs/v1/mcp-servers** ```json { "app_agent_id": "gid-of-app-agent", "token_introspect_id": "gid-of-token-introspect", "project_id": "gid-of-project", "name": "mcp-server-name", "display_name": "MCP Server name", "description": "MCP Server configuration description", "enabled": true, "scopes_supported": [ "name", "email" ] } ``` ### Step 2 Capture the nodes needed for this use case (replace bearer-token-sub with actual Bearer token sub). **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "bearer-token-sub", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "alice@email.com" }, { "type": "given_name", "value": "Alice" }, { "type": "last_name", "value": "Smith" } ] }, { "external_id": "knightrider", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "knightrider@demo.com" }, { "type": "name", "value": "Michael Knight" } ] }, { "external_id": "satchmo", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "satchmo@demo.com" }, { "type": "name", "value": "Louis Armstrong" } ] }, { "external_id": "karel", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "karel@demo.com" }, { "type": "name", "value": "Karel Plihal" } ] }, { "external_id": "kitt", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "pontiac" }, { "type": "model", "value": "Firebird" } ] }, { "external_id": "cadillacv16", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "Cadillac" }, { "type": "model", "value": "V-16" } ] }, { "external_id": "harmonika", "type": "Bus", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "Ikarus" }, { "type": "model", "value": "280" } ] }, { "external_id": "listek", "type": "Ticket", "is_identity": false }, { "external_id": "airbook-xyz", "type": "Laptop", "is_identity": false }, { "external_id": "ole", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "ole@yahoo.co.uk" }, { "type": "given_name", "value": "ole" }, { "type": "last_name", "value": "einar" } ] }, { "external_id": "cb2563", "type": "PaymentMethod", "properties": [ { "type": "payment_name", "value": "Credit Card Parking" }, { "type": "preference", "value": "Pay as you go" } ] }, { "external_id": "carOle", "type": "Vehicle", "properties": [ { "type": "category", "value": "Car" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "pcfjnm78" } ] }, { "external_id": "licenseOle", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "AL98745", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "licenseAlice", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "BTYUMN", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "loyalty1", "type": "Loyalty", "properties": [ { "type": "name", "value": "Parking Loyalty Plan" } ] }, { "external_id": "consent1", "type": "ConsentPayment", "properties": [ { "type": "name", "value": "Consent Parking" } ] }, { "external_id": "companyParking", "type": "Company", "properties": [ { "type": "name", "value": "City Parking Inc" } ] }, { "external_id": "applicationParking", "type": "Application", "properties": [ { "type": "name", "value": "City Mall Parking" } ] } ] } ``` ### Step 2 Capture the relationships needed for this use case (replace bearer-token-sub with actual Bearer token sub). **POST https://eu.api.indykite.com/capture/v1/relationships/** ```json { "relationships": [ { "source": { "external_id": "knightrider", "type": "Person" }, "target": { "external_id": "kitt", "type": "Car" }, "type": "DRIVES" }, { "source": { "external_id": "satchmo", "type": "Person" }, "target": { "external_id": "cadillacv16", "type": "Car" }, "type": "DRIVES" }, { "source": { "external_id": "karel", "type": "Person" }, "target": { "external_id": "listek", "type": "Ticket" }, "type": "HAS" }, { "source": { "external_id": "listek", "type": "Ticket" }, "target": { "external_id": "harmonika", "type": "Bus" }, "type": "FOR" }, { "source": { "external_id": "karel", "type": "Person" }, "target": { "external_id": "airbook-xyz", "type": "Laptop" }, "type": "OWNS" }, { "source": { "external_id": "bearer-token-sub", "type": "Person" }, "target": { "external_id": "airbook-xyz", "type": "Laptop" }, "type": "OWNS" }, { "source": { "external_id": "knightrider", "type": "Person" }, "target": { "external_id": "kitt", "type": "Car" }, "type": "OWNS" }, { "source": { "external_id": "bearer-token-sub", "type": "Person" }, "target": { "external_id": "cadillacv16", "type": "Car" }, "type": "DRIVES" }, { "source": { "external_id": "ole", "type": "Person" }, "target": { "external_id": "cb2563", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "ole", "type": "Person" }, "target": { "external_id": "carOle", "type": "Car" }, "type": "OWNS" }, { "source": { "external_id": "ole", "type": "Person" }, "target": { "external_id": "loyalty1", "type": "Loyalty" }, "type": "IS_MEMBER" }, { "source": { "external_id": "bearer-token-sub", "type": "Person" }, "target": { "external_id": "loyalty1", "type": "Loyalty" }, "type": "IS_MEMBER" }, { "source": { "external_id": "ole", "type": "Person" }, "target": { "external_id": "consent1", "type": "ConsentPayment" }, "type": "GRANTED" }, { "source": { "external_id": "carOle", "type": "Car" }, "target": { "external_id": "licenseOle", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "consent1", "type": "ConsentPayment" }, "target": { "external_id": "cb2563", "type": "PaymentMethod" }, "type": "GRANTED" }, { "source": { "external_id": "companyParking", "type": "Company" }, "target": { "external_id": "applicationParking", "type": "Application" }, "type": "OWNS" }, { "source": { "external_id": "applicationParking", "type": "Application" }, "target": { "external_id": "consent1", "type": "ConsentPayment" }, "type": "USES" }, { "source": { "external_id": "bearer-token-sub", "type": "Person" }, "target": { "external_id": "cb8521", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "bearer-token-sub", "type": "Person" }, "target": { "external_id": "carAlice", "type": "Car" }, "type": "OWNS" }, { "source": { "external_id": "bearer-token-sub", "type": "Person" }, "target": { "external_id": "consent1", "type": "ConsentPayment" }, "type": "GRANTED" }, { "source": { "external_id": "carAlice", "type": "Car" }, "target": { "external_id": "licenseAlice", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "consent1", "type": "ConsentPayment" }, "target": { "external_id": "cb8521", "type": "PaymentMethod" }, "type": "GRANTED" } ] } ``` ### Step 3 Create a CIQ Policy which designates the Subject node, the cypher and the nodes allowed to be read. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"policy\":{\"meta\":{\"policy_version\":\"1.0-ciq\"},\"subject\":{\"type\":\"Person\"},\"condition\":{\"cypher\":\"MATCH (subject:Person) MATCH (app:Application)-[:USES]->(consentpayment:ConsentPayment)<-[:GRANTED]-(subject)-[:HAS]->(paymentmethod:PaymentMethod) MATCH (subject)-[:IS_MEMBER]->(loyalty:Loyalty) MATCH (subject)-[:OWNS]->(car:Car)-[:HAS]->(ln:LicenseNumber)\",\"filter\":[{\"operator\":\"AND\",\"operands\":[{\"attribute\":\"subject.external_id\",\"operator\":\"=\",\"value\":\"$subject_external_id\"},{\"attribute\":\"subject.property.email\",\"operator\":\"=\",\"value\":\"$subject_email\"}]}]},\"allowed_reads\":{\"nodes\":[\"ln.*\",\"app.*\",\"paymentmethod.external_id\"]}}}", "status": "ACTIVE", "tags": [] } ``` ### Step 4 Create a CIQ Query in the context of the policy. In the description, give all the necessary information an agent would need to know to call the ciq_execute tool. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "Call tool 'ciq_execute' with arguments : id: \"\", input_params: {subject_external_id: (required) must match Bearer token 'sub', subject_email: (required), license: (required) car license plate}. Auth: Bearer token required, token subject = subject_external_id. Returns: payment_method_external_id", "display_name": "knowledge query name", "name": "knowledge-query-name", "policy_id": "your_policy_gid", "query": "{\"nodes\":[\"paymentmethod.external_id\"],\"filter\":{\"attribute\":\"ln.property.number\",\"operator\":\"=\",\"value\":\"$license\"}}", "status": "ACTIVE" } ``` ### Step 5 Create a KBAC Policy which designates the conditions to drive a car. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"meta\":{\"policy_version\":\"2.0-kbac\"},\"subject\":{\"type\":\"Person\"},\"actions\":[\"CAN_DRIVE\"],\"resource\":{\"type\":\"Car\"},\"condition\":{\"cypher\":\"MATCH (subject:Person)-[:DRIVES]->(resource:Car)\"}}", "status": "ACTIVE", "tags": [] } ``` ### Step 6 Initialize a session with the MCP server. Returns capabilities and session id. **POST https://eu.mcp.indykite.com/mcp/v1/** ```jsonrpc curl -v -i -X POST https://eu.mcp.indykite.com/mcp/v1/ -H "Content-Type: application/json" -H "Authorization: Bearer $BEARER_TOKEN" -d '{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-11-25", "capabilities": {}, "clientInfo": { "name": "curl", "version": "1.0" } } }' ``` Check if session is initialized with the MCP server. **POST https://eu.mcp.indykite.com/mcp/v1/** ```jsonrpc curl -v -i -X POST https://eu.mcp.indykite.com/mcp/v1/ -H "Content-Type: application/json" -H "Authorization: Bearer $BEARER_TOKEN" -H "Mcp-Session-Id: $SESSION_ID" -d '{ "jsonrpc": "2.0", "id": 1, "method": "notifications/initialized", "params": { "protocolVersion": "2025-11-25", "capabilities": {}, "clientInfo": { "name": "curl", "version": "1.0" } } }'1 ``` List resources available in the MCP server. **POST https://eu.mcp.indykite.com/mcp/v1/** ```jsonrpc curl -v -i -X POST https://eu.mcp.indykite.com/mcp/v1/ -H "Content-Type: application/json" -H "Authorization: Bearer $BEARER_TOKEN" -H "Mcp-Session-Id: $SESSION_ID" -d '{ "jsonrpc": "2.0", "id": 2, "method": "resources/list", "params": {} }' ``` List tools available in the MCP server. **POST https://eu.mcp.indykite.com/mcp/v1/** ```jsonrpc curl -v -i -X POST https://eu.mcp.indykite.com/mcp/v1/ -H "Content-Type: application/json" -H "Authorization: Bearer $BEARER_TOKEN" -H "Mcp-Session-Id: $SESSION_ID" -d '{ "jsonrpc": "2.0", "id": 3, "method": "tools/list", "params": {} }' ``` List tools response. ```jsonrpc { "jsonrpc": "2.0", "id": 3, "result": { "tools": [ { "name": "authzen_evaluate", "description": "Evaluate access with AuthZEN evaluation endpoint", "inputSchema": { "type": "object", "required": [ "subject_type", "subject_id", "resource_type", "resource_id", "action_name" ], "properties": { "subject_type": { "type": "string", "description": "required, description: Type of subject" }, "subject_id": { "type": "string", "description": "required, description: ID of subject" }, "resource_type": { "type": "string", "description": "required, description: Type of resource" }, "resource_id": { "type": "string", "description": "required, description: ID of resource" }, "action_name": { "type": "string", "description": "required, description: Action name" }, "context": { "type": "object", "description": "Optional context", "additionalProperties": true } }, "additionalProperties": false } }, { "name": "authzen_evaluations", "description": "Execute multiple access evaluations in a single request", "inputSchema": { "type": "object", "required": ["evaluations"], "properties": { "evaluations": { "type": "array", "description": "required, description: Evaluations", "items": { "type": "object", "additionalProperties": true } }, "action_name": { "type": ["null", "string"], "description": "Default action name" }, "resource_type": { "type": ["null", "string"], "description": "Default resource type" }, "resource_id": { "type": ["null", "string"], "description": "Default resource ID" }, "subject_type": { "type": ["null", "string"], "description": "Default subject type" }, "subject_id": { "type": ["null", "string"], "description": "Default subject ID" }, "context": { "type": "object", "description": "Optional context", "additionalProperties": true } }, "additionalProperties": false } }, { "name": "authzen_search_action", "description": "Search for all actions a subject can perform on a resource", "inputSchema": { "type": "object", "required": [ "subject_type", "subject_id", "resource_type", "resource_id" ], "properties": { "subject_type": { "type": "string", "description": "required, description: Type of subject" }, "subject_id": { "type": "string", "description": "required, description: ID of subject" }, "resource_type": { "type": "string", "description": "required, description: Type of resource" }, "resource_id": { "type": "string", "description": "required, description: ID of resource" }, "context": { "type": "object", "description": "Optional context", "additionalProperties": true }, "page": { "type": "object", "description": "Pagination parameters", "additionalProperties": true } }, "additionalProperties": false } }, { "name": "authzen_search_resource", "description": "Search for resources a subject can access with a specified action", "inputSchema": { "type": "object", "required": [ "subject_type", "subject_id", "resource_type", "action_name" ], "properties": { "subject_type": { "type": "string", "description": "required, description: Type of subject" }, "subject_id": { "type": "string", "description": "required, description: ID of subject" }, "resource_type": { "type": "string", "description": "required, description: Type of resource" }, "action_name": { "type": "string", "description": "required, description: Action name" }, "context": { "type": "object", "description": "Optional context", "additionalProperties": true }, "page": { "type": "object", "description": "Pagination parameters", "additionalProperties": true } }, "additionalProperties": false } }, { "name": "ciq_execute", "description": "Execute a ContX IQ (CIQ) query by id (knowledge_query id)", "inputSchema": { "type": "object", "required": ["id"], "properties": { "id": { "type": "string", "description": "required, description: Knowledge query ID" }, "input_params": { "type": "object", "description": "Optional input parameters", "additionalProperties": true }, "page_size": { "type": ["null", "integer"], "description": "Optional page size (default 100)" }, "page_token": { "type": ["null", "integer"], "description": "Optional pagination token" } }, "additionalProperties": false } } ] } } ``` Call the knowledge-queries resource. **POST https://eu.mcp.indykite.com/mcp/v1/** ```jsonrpc curl -v -i -X POST https://eu.mcp.indykite.com/mcp/v1/ -H "Content-Type: application/json" -H "Authorization: Bearer $BEARER_TOKEN" -H "Mcp-Session-Id: $SESSION_ID" -d '{ "jsonrpc": "2.0", "id": 4, "method": "resources/read", "params": { "uri": "indykite://knowledge-queries/" } }' ``` Call the knowledge-queries resource. ```jsonrpc { "jsonrpc":"2.0", "id":4, "result":{ "contents":[ { "uri":"indykite://knowledge-queries/", "mimeType":"application/json", "text":{ "knowledge_queries": [ { "id": "", "description": "Call tool 'ciq_execute' with arguments : id: "", input_params: {subject_external_id: (required) must match Bearer token 'sub', subject_email: (required), license: (required) car license plate}. Auth: Bearer token required, token subject = subject_external_id. Returns: payment_method_external_id", "status": "STATUS_ACTIVE" } ], "mcp_url": "https://eu.mcp.indykite.com/mcp/v1/>", "total_count": 1 } } ] } } ``` Call the authzen_evaluate tool to check if according to the KBAC policy, the subject authorized by the Bearer access token can drive the car. **POST https://eu.mcp.indykite.com/mcp/v1/** ```jsonrpc curl -v -i -X POST https://eu.mcp.indykite.com/mcp/v1/ -H "Content-Type: application/json" -H "Authorization: Bearer $BEARER_TOKEN" -H "Mcp-Session-Id: $SESSION_ID" -d '{ "jsonrpc": "2.0", "id": 5, "method": "tools/call", "params": { "name": "authzen_evaluate", "arguments": { "subject_type": "Person", "subject_id": , "resource_type": "Car", "resource_id": "cadillacv16", "action_name": "CAN_DRIVE" } } }' ``` Authzen_evaluate tool response. **POST https://eu.mcp.indykite.com/mcp/v1/** ```jsonrpc { "jsonrpc":"2.0", "id":5, "result":{ "content":[ { "type":"text", "text": {"decision":true} } ] } } ``` Call the ciq execute tool to get the payment method designated by the CIQ policy for the subject authorized by the Bearer access token. **POST https://eu.mcp.indykite.com/mcp/v1/** ```jsonrpc curl -v -i -X POST https://eu.mcp.indykite.com/mcp/v1/ -H "Content-Type: application/json" -H "Authorization: Bearer $BEARER_TOKEN" -H "Mcp-Session-Id: $SESSION_ID" -d '{ "jsonrpc": "2.0", "id": 9, "method": "tools/call", "params": { "name": "ciq_execute", "arguments": { "id": "", "input_params": {"license": "BTYUMN","subject_external_id": ,"subject_email": "alice@email.com"}, "page_token": 1 } } }' ``` Ciq Execute tool response. **POST https://eu.mcp.indykite.com/mcp/v1/** ```jsonrpc { "jsonrpc":"2.0", "id":9, "result":{ "content":[{ "type":"text", "text":{ "data":[{ "nodes":{ "paymentmethod.external_id":"cb8521" } }] } }] } } ``` --- Source: https://developer.indykite.com/resources/mcp-1 --- # MCP Server: Stateless Tool Calls with Protocol 2026-07-28 > Demonstrates the stateless MCP protocol (revision 2026-07-28) against the IndyKite MCP server: no initialize handshake and no Mcp-Session-Id - every request is self-contained, carrying its protocol metadata in params._meta plus the standard MCP headers. Same environment and tools as the session-based MCP resource. **Category:** MCP **API:** MCP **Tags:** MCP, Model Context Protocol, Stateless Protocol, server/discover, ContX IQ, KBAC, authZEN, AI Integration, LLM Tools **Last Updated:** 2026-08-31 **OpenAPI Endpoints:** /configs/v1/mcp-servers, /capture/v1/nodes, /capture/v1/relationships, /configs/v1/authorization-policies, /configs/v1/knowledge-queries **Related Guides:** /guides/guide-mcp ## Summary This example demonstrates the stateless MCP protocol (revision 2026-07-28) for AI/LLM integration. Protocol used here: Stateless MCP protocol, revision 2026-07-28 - no initialize/initialized handshake and no Mcp-Session-Id header. Each request stands on its own: - In the body, params._meta carries "io.modelcontextprotocol/protocolVersion", "io.modelcontextprotocol/clientCapabilities" and optionally "io.modelcontextprotocol/clientInfo". - In the headers: Mcp-Protocol-Version, Mcp-Method (must equal the JSON-RPC method), and Mcp-Name for tools/call (the tool name) and resources/read (the resource URI). No session is created: responses carry no Mcp-Session-Id header, and a stale Mcp-Session-Id sent by a mixed-version client is ignored when _meta says 2026-07-28 or later. New method: - server/discover - returns the server's capabilities and the protocol revisions it accepts (result.supportedVersions), so a client can decide which style to speak. Workflow: 1. Call server/discover to confirm 2026-07-28 is supported 2. List available tools (authzen_evaluate, ciq_execute) - statelessly 3. Execute tools with parameters, one self-contained request per call Requesting an unsupported revision returns HTTP 400 with JSON-RPC error -32022 ("unsupported protocol version") naming the requested and supported versions. The session-based flow (see the companion MCP resource) keeps working unchanged - both styles expose the same tools and resources. ## Use Case Scenario: A serverless AI agent (one short-lived invocation per user question) needs to check permissions and retrieve authorized data - holding an MCP session between invocations is impractical. Stateless integration flow: 1. Agent cold-starts, calls server/discover -> sees "2026-07-28" in supportedVersions 2. User asks: "Can I drive the company car?" 3. Agent sends one self-contained tools/call request: - Headers: Mcp-Protocol-Version: 2026-07-28, Mcp-Method: tools/call, Mcp-Name: authzen_evaluate - Body params: the authzen_evaluate arguments plus the _meta protocol object 4. MCP evaluates against the KBAC policy and returns {decision: true} 5. The invocation ends - nothing to tear down, no session to expire For data retrieval the same shape applies: one stateless tools/call to ciq_execute with the knowledge query ID and input_params, authorized by the caller's Bearer token. ## Requirements Prerequisites (identical environment to the session-based MCP resource): - ServiceAccount credentials: For creating policies, queries, and the MCP Server configuration (Bearer token) - AppAgent: Referenced by the MCP Server config as the identity the server uses to call IndyKite APIs at runtime (resolved server-side) - User access token: JWT for the subject performing authorization checks - Token Introspect configuration: Required to validate user access tokens (referenced by the MCP Server config) MCP Server endpoint: - URL: https://eu.mcp.indykite.com/mcp/v1/{project_gid} - Protocol: JSON-RPC 2.0 over HTTP POST - stateless MCP protocol, revision 2026-07-28 (params._meta + Mcp-Protocol-Version / Mcp-Method / Mcp-Name headers; no session) Required configurations: - MCP Server: Binds the runtime MCP endpoint to an AppAgent + Token Introspect, declares supported OAuth scopes - KBAC Policy: Defines authorization rules (e.g., who can drive cars) - ContX IQ Policy and Query: Defines data access rules and queries ## Steps Step 1: Create the MCP Server configuration - Authentication: ServiceAccount credential (Bearer token) - Action: POST to /configs/v1/mcp-servers - Input: AppAgent ID, Token Introspect ID, project ID, supported OAuth scopes - Result: MCP Server config ID returned; the runtime MCP endpoint will use this binding Step 2: Ingest Graph Data - Authentication: AppAgent credential (X-IK-ClientKey header) - Action: POST nodes (Person, Car) and relationships (DRIVES) - Note: Replace bearer-token-sub with actual user's token subject claim - Result: Graph ready for authorization queries Step 3: Create ContX IQ Policy - Authentication: ServiceAccount credential (Bearer token) - Action: POST policy for data access through MCP - Result: Policy ID returned Step 4: Create ContX IQ Knowledge Query - Authentication: ServiceAccount credential (Bearer token) - Action: POST query with descriptive name and parameters - Important: Include detailed description for AI agent understanding - Result: Query ID available via MCP resources Step 5: Create KBAC Policy - Authentication: ServiceAccount credential (Bearer token) - Action: POST authZEN policy (e.g., Person can DRIVE Car if DRIVES relationship exists) - Result: Policy ID returned, available via authzen_evaluate tool Step 6: Use the MCP Server statelessly (protocol 2026-07-28) - Discover: "server/discover" returns capabilities and supportedVersions - no session is created - List tools: "tools/list" with the _meta object and Mcp-Method header - Read the knowledge-queries resource: "resources/read" with Mcp-Name set to the resource URI - Execute authzen_evaluate: one self-contained "tools/call" with Mcp-Name: authzen_evaluate - Execute ciq_execute: one self-contained "tools/call" with Mcp-Name: ciq_execute - Every request repeats the Bearer token and the _meta protocol object; no Mcp-Session-Id anywhere ## Code Examples ### Step 1 Create the MCP Server configuration. This binds the runtime MCP endpoint (https://.mcp.indykite.com/mcp/v1/) to an AppAgent (the identity the server uses to call IndyKite APIs at runtime, resolved server-side) and a Token Introspect config (used to validate Bearer tokens), and declares the OAuth scopes the server advertises in its .well-known/oauth-protected-resource metadata. Replace gid-of-app-agent and gid-of-token-introspect with the IDs returned in the environment-setup steps. **POST https://eu.api.indykite.com/configs/v1/mcp-servers** ```json { "app_agent_id": "gid-of-app-agent", "token_introspect_id": "gid-of-token-introspect", "project_id": "gid-of-project", "name": "mcp-server-name", "display_name": "MCP Server name", "description": "MCP Server configuration description", "enabled": true, "scopes_supported": [ "name", "email" ] } ``` ### Step 2 Capture the nodes needed for this use case (replace bearer-token-sub with actual Bearer token sub). **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "bearer-token-sub", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "alice@email.com" }, { "type": "given_name", "value": "Alice" }, { "type": "last_name", "value": "Smith" } ] }, { "external_id": "knightrider", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "knightrider@demo.com" }, { "type": "name", "value": "Michael Knight" } ] }, { "external_id": "satchmo", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "satchmo@demo.com" }, { "type": "name", "value": "Louis Armstrong" } ] }, { "external_id": "karel", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "karel@demo.com" }, { "type": "name", "value": "Karel Plihal" } ] }, { "external_id": "kitt", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "pontiac" }, { "type": "model", "value": "Firebird" } ] }, { "external_id": "cadillacv16", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "Cadillac" }, { "type": "model", "value": "V-16" } ] }, { "external_id": "harmonika", "type": "Bus", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "Ikarus" }, { "type": "model", "value": "280" } ] }, { "external_id": "listek", "type": "Ticket", "is_identity": false }, { "external_id": "airbook-xyz", "type": "Laptop", "is_identity": false }, { "external_id": "ole", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "ole@yahoo.co.uk" }, { "type": "given_name", "value": "ole" }, { "type": "last_name", "value": "einar" } ] }, { "external_id": "cb2563", "type": "PaymentMethod", "properties": [ { "type": "payment_name", "value": "Credit Card Parking" }, { "type": "preference", "value": "Pay as you go" } ] }, { "external_id": "carOle", "type": "Vehicle", "properties": [ { "type": "category", "value": "Car" }, { "type": "is_active", "value": true }, { "type": "vin", "value": "pcfjnm78" } ] }, { "external_id": "licenseOle", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "AL98745", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "licenseAlice", "type": "LicenseNumber", "properties": [ { "type": "status", "value": "Active" }, { "type": "number", "value": "BTYUMN", "metadata": { "assurance_level": 3, "source": "BRREG" } } ] }, { "external_id": "loyalty1", "type": "Loyalty", "properties": [ { "type": "name", "value": "Parking Loyalty Plan" } ] }, { "external_id": "consent1", "type": "ConsentPayment", "properties": [ { "type": "name", "value": "Consent Parking" } ] }, { "external_id": "companyParking", "type": "Company", "properties": [ { "type": "name", "value": "City Parking Inc" } ] }, { "external_id": "applicationParking", "type": "Application", "properties": [ { "type": "name", "value": "City Mall Parking" } ] } ] } ``` ### Step 2 Capture the relationships needed for this use case (replace bearer-token-sub with actual Bearer token sub). **POST https://eu.api.indykite.com/capture/v1/relationships/** ```json { "relationships": [ { "source": { "external_id": "knightrider", "type": "Person" }, "target": { "external_id": "kitt", "type": "Car" }, "type": "DRIVES" }, { "source": { "external_id": "satchmo", "type": "Person" }, "target": { "external_id": "cadillacv16", "type": "Car" }, "type": "DRIVES" }, { "source": { "external_id": "karel", "type": "Person" }, "target": { "external_id": "listek", "type": "Ticket" }, "type": "HAS" }, { "source": { "external_id": "listek", "type": "Ticket" }, "target": { "external_id": "harmonika", "type": "Bus" }, "type": "FOR" }, { "source": { "external_id": "karel", "type": "Person" }, "target": { "external_id": "airbook-xyz", "type": "Laptop" }, "type": "OWNS" }, { "source": { "external_id": "bearer-token-sub", "type": "Person" }, "target": { "external_id": "airbook-xyz", "type": "Laptop" }, "type": "OWNS" }, { "source": { "external_id": "knightrider", "type": "Person" }, "target": { "external_id": "kitt", "type": "Car" }, "type": "OWNS" }, { "source": { "external_id": "bearer-token-sub", "type": "Person" }, "target": { "external_id": "cadillacv16", "type": "Car" }, "type": "DRIVES" }, { "source": { "external_id": "ole", "type": "Person" }, "target": { "external_id": "cb2563", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "ole", "type": "Person" }, "target": { "external_id": "carOle", "type": "Car" }, "type": "OWNS" }, { "source": { "external_id": "ole", "type": "Person" }, "target": { "external_id": "loyalty1", "type": "Loyalty" }, "type": "IS_MEMBER" }, { "source": { "external_id": "bearer-token-sub", "type": "Person" }, "target": { "external_id": "loyalty1", "type": "Loyalty" }, "type": "IS_MEMBER" }, { "source": { "external_id": "ole", "type": "Person" }, "target": { "external_id": "consent1", "type": "ConsentPayment" }, "type": "GRANTED" }, { "source": { "external_id": "carOle", "type": "Car" }, "target": { "external_id": "licenseOle", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "consent1", "type": "ConsentPayment" }, "target": { "external_id": "cb2563", "type": "PaymentMethod" }, "type": "GRANTED" }, { "source": { "external_id": "companyParking", "type": "Company" }, "target": { "external_id": "applicationParking", "type": "Application" }, "type": "OWNS" }, { "source": { "external_id": "applicationParking", "type": "Application" }, "target": { "external_id": "consent1", "type": "ConsentPayment" }, "type": "USES" }, { "source": { "external_id": "bearer-token-sub", "type": "Person" }, "target": { "external_id": "cb8521", "type": "PaymentMethod" }, "type": "HAS" }, { "source": { "external_id": "bearer-token-sub", "type": "Person" }, "target": { "external_id": "carAlice", "type": "Car" }, "type": "OWNS" }, { "source": { "external_id": "bearer-token-sub", "type": "Person" }, "target": { "external_id": "consent1", "type": "ConsentPayment" }, "type": "GRANTED" }, { "source": { "external_id": "carAlice", "type": "Car" }, "target": { "external_id": "licenseAlice", "type": "LicenseNumber" }, "type": "HAS" }, { "source": { "external_id": "consent1", "type": "ConsentPayment" }, "target": { "external_id": "cb8521", "type": "PaymentMethod" }, "type": "GRANTED" } ] } ``` ### Step 3 Create a CIQ Policy which designates the Subject node, the cypher and the nodes allowed to be read. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"policy\":{\"meta\":{\"policy_version\":\"1.0-ciq\"},\"subject\":{\"type\":\"Person\"},\"condition\":{\"cypher\":\"MATCH (subject:Person) MATCH (app:Application)-[:USES]->(consentpayment:ConsentPayment)<-[:GRANTED]-(subject)-[:HAS]->(paymentmethod:PaymentMethod) MATCH (subject)-[:IS_MEMBER]->(loyalty:Loyalty) MATCH (subject)-[:OWNS]->(car:Car)-[:HAS]->(ln:LicenseNumber)\",\"filter\":[{\"operator\":\"AND\",\"operands\":[{\"attribute\":\"subject.external_id\",\"operator\":\"=\",\"value\":\"$subject_external_id\"},{\"attribute\":\"subject.property.email\",\"operator\":\"=\",\"value\":\"$subject_email\"}]}]},\"allowed_reads\":{\"nodes\":[\"ln.*\",\"app.*\",\"paymentmethod.external_id\"]}}}", "status": "ACTIVE", "tags": [] } ``` ### Step 4 Create a CIQ Query in the context of the policy. In the description, give all the necessary information an agent would need to know to call the ciq_execute tool. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "Call tool 'ciq_execute' with arguments : id: \"\", input_params: {subject_external_id: (required) must match Bearer token 'sub', subject_email: (required), license: (required) car license plate}. Auth: Bearer token required, token subject = subject_external_id. Returns: payment_method_external_id", "display_name": "knowledge query name", "name": "knowledge-query-name", "policy_id": "your_policy_gid", "query": "{\"nodes\":[\"paymentmethod.external_id\"],\"filter\":{\"attribute\":\"ln.property.number\",\"operator\":\"=\",\"value\":\"$license\"}}", "status": "ACTIVE" } ``` ### Step 5 Create a KBAC Policy which designates the conditions to drive a car. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "description of policy", "display_name": "policy name", "name": "policy-name", "policy": "{\"meta\":{\"policy_version\":\"2.0-kbac\"},\"subject\":{\"type\":\"Person\"},\"actions\":[\"CAN_DRIVE\"],\"resource\":{\"type\":\"Car\"},\"condition\":{\"cypher\":\"MATCH (subject:Person)-[:DRIVES]->(resource:Car)\"}}", "status": "ACTIVE", "tags": [] } ``` ### Step 6 Discover the server. server/discover is a stateless-protocol method: it returns the server's capabilities and the protocol revisions it accepts, without creating a session. Note the _meta object in params and the Mcp-Protocol-Version / Mcp-Method headers - the stateless request signature used by every call below. **POST https://eu.mcp.indykite.com/mcp/v1/** ```jsonrpc curl -v -i -X POST https://eu.mcp.indykite.com/mcp/v1/ -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" -H "Authorization: Bearer $BEARER_TOKEN" -H "Mcp-Protocol-Version: 2026-07-28" -H "Mcp-Method: server/discover" -d '{ "jsonrpc": "2.0", "id": 1, "method": "server/discover", "params": { "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {}, "io.modelcontextprotocol/clientInfo": {"name": "curl", "version": "1.0"} } } }' ``` server/discover response: the supported protocol revisions and capabilities. No Mcp-Session-Id header is returned - no session exists. ```jsonrpc { "jsonrpc": "2.0", "id": 1, "result": { "resultType": "complete", "_meta": { "io.modelcontextprotocol/serverInfo": {"name": "", "version": ""} }, "supportedVersions": ["2026-07-28", "2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"], "capabilities": { "resources": {}, "tools": {} }, "instructions": "" } } ``` List tools statelessly. Same tools as the session-based flow (authzen_evaluate, authzen_evaluations, authzen_search_action, authzen_search_resource, ciq_execute) - only the request framing differs. **POST https://eu.mcp.indykite.com/mcp/v1/** ```jsonrpc curl -v -i -X POST https://eu.mcp.indykite.com/mcp/v1/ -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" -H "Authorization: Bearer $BEARER_TOKEN" -H "Mcp-Protocol-Version: 2026-07-28" -H "Mcp-Method: tools/list" -d '{ "jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": { "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {} } } }' ``` Read the knowledge-queries resource statelessly. For resources/read the Mcp-Name header carries the resource URI and must match the uri in the body. **POST https://eu.mcp.indykite.com/mcp/v1/** ```jsonrpc curl -v -i -X POST https://eu.mcp.indykite.com/mcp/v1/ -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" -H "Authorization: Bearer $BEARER_TOKEN" -H "Mcp-Protocol-Version: 2026-07-28" -H "Mcp-Method: resources/read" -H "Mcp-Name: indykite://knowledge-queries/" -d '{ "jsonrpc": "2.0", "id": 3, "method": "resources/read", "params": { "uri": "indykite://knowledge-queries/", "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {} } } }' ``` Call the authzen_evaluate tool in one self-contained request: Mcp-Method mirrors the JSON-RPC method, Mcp-Name names the tool, and _meta sits in params next to the tool arguments. The KBAC policy decides whether the Bearer token's subject can drive the car. **POST https://eu.mcp.indykite.com/mcp/v1/** ```jsonrpc curl -v -i -X POST https://eu.mcp.indykite.com/mcp/v1/ -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" -H "Authorization: Bearer $BEARER_TOKEN" -H "Mcp-Protocol-Version: 2026-07-28" -H "Mcp-Method: tools/call" -H "Mcp-Name: authzen_evaluate" -d '{ "jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": { "name": "authzen_evaluate", "arguments": { "subject_type": "Person", "subject_id": , "resource_type": "Car", "resource_id": "cadillacv16", "action_name": "CAN_DRIVE" }, "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {} } } }' ``` Authzen_evaluate tool response - identical to the session-based flow, with no Mcp-Session-Id header. ```jsonrpc { "jsonrpc":"2.0", "id":4, "result":{ "content":[ { "type":"text", "text": {"decision":true} } ] } } ``` Call the ciq_execute tool statelessly to get the payment method designated by the CIQ policy for the subject authorized by the Bearer access token. **POST https://eu.mcp.indykite.com/mcp/v1/** ```jsonrpc curl -v -i -X POST https://eu.mcp.indykite.com/mcp/v1/ -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" -H "Authorization: Bearer $BEARER_TOKEN" -H "Mcp-Protocol-Version: 2026-07-28" -H "Mcp-Method: tools/call" -H "Mcp-Name: ciq_execute" -d '{ "jsonrpc": "2.0", "id": 5, "method": "tools/call", "params": { "name": "ciq_execute", "arguments": { "id": "", "input_params": {"license": "BTYUMN","subject_external_id": ,"subject_email": "alice@email.com"} }, "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {} } } }' ``` Error contract: requesting a protocol revision the server does not support returns HTTP 400 with JSON-RPC error -32022, naming the requested and supported versions. ```jsonrpc { "jsonrpc": "2.0", "id": 2, "error": { "code": -32022, "message": "unsupported protocol version", "data": { "requested": "2099-01-01", "supported": ["2026-07-28", "2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"] } } } ``` ## Common Errors ### 400: unsupported protocol version (JSON-RPC error -32022) **Solution:** The requested _meta protocolVersion is not supported by the server. Call server/discover and use one of the revisions listed in result.supportedVersions (2026-07-28 for the stateless style). ### 400: missing required Mcp-Method header / missing required Mcp-Name header (JSON-RPC error -32020) **Solution:** For protocol 2026-07-28 every request must send an Mcp-Method header equal to the JSON-RPC method in the body; tools/call, resources/read and prompts/get additionally require a matching Mcp-Name header. --- Source: https://developer.indykite.com/resources/mcp-2 --- # Data Residency: Composite-Database Project with Location-Routed Ingestion > Set up a project backed by a customer-hosted composite database, create the application hierarchy and credentials, then ingest nodes routed to specific locations and relationships stored in the global database. **Category:** Environment **API:** Environment **Tags:** Data Residency, Composite Database, Environment Setup, Data Ingestion, Capture API **Last Updated:** 2026-07-06 **OpenAPI Endpoints:** /configs/v1/projects, /configs/v1/applications, /configs/v1/application-agents, /configs/v1/application-agent-credentials, /capture/v1/nodes, /capture/v1/relationships **Related Guides:** /guides/guide-data-residency, /guides/guide-environment, /guides/guide-credentials ## Summary This example walks through the full data-residency flow end to end: 1. Create a Project connected to your composite database (composite_db_name + alias_mapping) 2. Poll the project until ikg_status is ACTIVE 3. Create an Application, an Application Agent, and Application Agent Credentials 4. Upsert nodes with a per-node location so each node's data is stored in its constituent database 5. Upsert relationships with use_global_db so cross-location connections land in the global database The result is one logical Identity Knowledge Graph whose records physically live in the locations you chose. ## Use Case Scenario: Your application serves customers in two jurisdictions and each customer's personal data must stay in its region, while shared infrastructure records and the relationships connecting everything remain globally reachable: - Person(alice) stored in the "east" location database - Person(karel) stored in the "west" location database - Device(hq-badge-printer) stored in the "global" database - Car(car1) has no location and goes to the default database - OWNS / USES relationships stored in the global database, connecting nodes across locations Applications keep querying a single graph; the composite database handles the placement. Note on reads: ContX IQ queries and KBAC/AuthZEN checks with 2.0-kbac policies execute against the default constituent only (db_connection.name - the global database in this example). Decisions that traverse proxy nodes and relationships keep working; the located property data (alice's and karel's personal details) is not visible to those queries or policy conditions. To evaluate authorization inside a location constituent, author the policy as 3.0-kbac with USE graph.byName() routing - see resources authz-7 and authz-8. ## Requirements Prerequisites: - ServiceAccount credentials: Created in IndyKite Hub for your Organization - Organization ID: Your IndyKite organization identifier - A customer-hosted Neo4j deployment where the composite database and its constituent databases (db1, db2, db3 in this example) already exist. IndyKite connects to them and configures routing - it does not create the databases. Composite databases are available for customer-hosted IKGs only. Required API access: - POST /configs/v1/projects (create project) - GET /configs/v1/projects/{id} (poll ikg_status) - POST /configs/v1/applications (create application) - POST /configs/v1/application-agents (create agent) - POST /configs/v1/application-agent-credentials (generate credentials) - POST /capture/v1/nodes (upsert located nodes) - POST /capture/v1/relationships (upsert relationships to the global DB) ## Steps Step 1: Create the Project with the composite connection - Authentication: ServiceAccount credential as Bearer token in Authorization header - Action: POST to /configs/v1/projects with db_connection carrying composite_db_name and alias_mapping - Input: alias_mapping maps logical locations to constituent databases (global=db1&east=db2&west=db3); db_connection.name is the default constituent used for nodes without a location - Result: Project ID returned; IKG provisioning starts asynchronously Step 2: Poll the Project until the IKG is ACTIVE - Authentication: ServiceAccount credential as Bearer token - Action: GET /configs/v1/projects/{id} until ikg_status is ACTIVE - Result: Response echoes db_connection.composite_db_name and alias_mapping; PENDING means provisioning is still running, FAILED means it did not complete Step 3: Create an Application - Authentication: ServiceAccount credential as Bearer token - Action: POST to /configs/v1/applications with the Project ID from Step 1 - Result: Application ID returned Step 4: Create an Application Agent - Authentication: ServiceAccount credential as Bearer token - Action: POST to /configs/v1/application-agents with the Application ID from Step 3 - Result: Application Agent ID returned Step 5: Generate Application Agent Credentials - Authentication: ServiceAccount credential as Bearer token - Action: POST to /configs/v1/application-agent-credentials with the Agent ID from Step 4 - Result: Credential JSON containing the API key (X-IK-ClientKey value); save it securely, it cannot be retrieved again Step 6: Upsert nodes with locations - Authentication: AppAgent credential as API key (header: X-IK-ClientKey) - Action: POST to /capture/v1/nodes with an optional location per node (a key of alias_mapping) - Result: Each located node's full data is stored in its constituent database and a lightweight proxy node (external_id, type, location only) is upserted into the global database; nodes without a location go to the default database Step 7: Upsert relationships into the global database - Authentication: AppAgent credential as API key (header: X-IK-ClientKey) - Action: POST to /capture/v1/relationships with use_global_db set to true - Result: Relationships are created in the global database between proxy nodes, connecting entities whose data lives in different locations ## Code Examples ### Step 1 POST request to create a project connected to a customer-hosted composite database. The composite database (ikcomposite) and its constituents (db1, db2, db3) must already exist in Neo4j. The alias_mapping keys (global, east, west) are the location values used later in Capture API requests. **POST https://eu.api.indykite.com/configs/v1/projects** ```json { "db_connection": { "url": "neo4j://your-neo4j-host:7687", "username": "neo4j", "password": "example-password", "name": "db1", "composite_db_name": "ikcomposite", "alias_mapping": "global=db1&east=db2&west=db3" }, "description": "Project with a customer-hosted composite database for data residency", "display_name": "Data residency project", "name": "residency-project", "organization_id": "gid-of-organization", "region": "europe-west1" } ``` ### Step 2 GET request to poll the project until ikg_status is ACTIVE. Provisioning runs asynchronously; do not ingest data before the IKG is active. Example response shown. **GET https://eu.api.indykite.com/configs/v1/projects/{id}** ```json { "id": "gid-of-project", "name": "residency-project", "region": "europe-west1", "ikg_status": "ACTIVE", "db_connection": { "url": "neo4j://your-neo4j-host:7687", "username": "neo4j", "name": "db1", "composite_db_name": "ikcomposite", "alias_mapping": "global=db1&east=db2&west=db3" } } ``` ### Step 3 POST request to create an application within the project. The application represents your software system that will use IndyKite services. **POST https://eu.api.indykite.com/configs/v1/applications** ```json { "description": "Application description", "display_name": "Application name", "name": "app-name", "project_id": "gid-of-project" } ``` ### Step 4 POST request to create an application agent. The agent is the authenticated identity that your application uses to call IndyKite APIs. **POST https://eu.api.indykite.com/configs/v1/application-agents** ```json { "api_permissions": [ "Authorization", "Capture", "ContXIQ", "EntityMatching" ], "application_id": "gid-of-application", "description": "App Agent description", "display_name": "App Agent name", "name": "app-agent-name" } ``` ### Step 5 POST request to generate credentials for the application agent. The response contains the API key to use in the X-IK-ClientKey header. Save this securely - it cannot be retrieved again. **POST https://eu.api.indykite.com/configs/v1/application-agent-credentials** ```json { "application_agent_id": "gid-of-app-agent", "display_name": "AppAgent Credentials name", "expire_time": "2026-12-31T12:34:56-01:00" } ``` ### Step 6 POST request to upsert nodes with per-node locations. alice is stored in the east constituent, karel in west, the shared device in global, and car1 (no location) in the default database. Proxy nodes for all located nodes are automatically upserted into the global database. **POST https://eu.api.indykite.com/capture/v1/nodes** ```json { "nodes": [ { "external_id": "alice", "is_identity": true, "type": "Person", "location": "east", "properties": [ { "type": "email", "value": "alice@email.com" }, { "type": "given_name", "value": "Alice" } ] }, { "external_id": "karel", "is_identity": true, "type": "Person", "location": "west", "properties": [ { "type": "email", "value": "karel@email.com" }, { "type": "given_name", "value": "Karel" } ] }, { "external_id": "hq-badge-printer", "is_identity": false, "type": "Device", "location": "global", "properties": [ { "type": "model", "value": "BP-200" } ] }, { "external_id": "car1", "is_identity": false, "type": "Car", "properties": [ { "type": "manufacturer", "value": "Pontiac" } ] } ] } ``` ### Step 7 POST request to upsert relationships with use_global_db. The OWNS and USES relationships are stored in the global database, connecting nodes whose full data lives in different location databases. **POST https://eu.api.indykite.com/capture/v1/relationships** ```json { "use_global_db": true, "relationships": [ { "source": { "external_id": "alice", "type": "Person" }, "target": { "external_id": "car1", "type": "Car" }, "type": "OWNS" }, { "source": { "external_id": "karel", "type": "Person" }, "target": { "external_id": "hq-badge-printer", "type": "Device" }, "type": "USES" } ] } ``` ## Common Errors ### 401: UNAUTHENTICATED **Solution:** Config API steps need the ServiceAccount Bearer token; Capture API steps need the AppAgent key in the X-IK-ClientKey header ### 400: INVALID_ARGUMENT (unknown location) **Solution:** The location value must be one of the keys defined in the project's alias_mapping (global, east, west in this example) ### 400: INVALID_ARGUMENT (composite database not configured) **Solution:** A location was sent to a project without composite_db_name set. Configure the composite connection on the project, or omit location ### 400: INVALID_ARGUMENT (db_connection rejected on update) **Solution:** db_connection can only be set on customer-hosted projects; IndyKite-managed projects cannot become composite --- Source: https://developer.indykite.com/resources/residency-1 --- # Trust Score: Filter CIQ Queries by Subject Trust Score Threshold > Configure a Trust Score Profile for Person, ingest two Persons with different metadata quality via the Capture API, let the profile compute a _TrustScore for each, then run a CIQ query that returns the caller's own profile only when their _final_score is at or above the requested threshold. **Category:** TrustScore **API:** Trust Score **Tags:** Trust Score, Trust Score Profile, Capture, ContX IQ Policy, ContX IQ Query, _TrustScore Node, Data Quality, subject.trust_score._final_score **Last Updated:** 2026-05-20 **OpenAPI Endpoints:** /configs/v1/trust-score-profiles, /capture/v1/nodes, /configs/v1/authorization-policies, /configs/v1/knowledge-queries, /contx-iq/v1/execute **Related Guides:** /guides/guide-trust-score, /guides/guide-contx-iq ## Summary End-to-end demonstration that the Trust Score lives on the node as a regular property and can be filtered on inside a CIQ policy. Pipeline: 1. Trust Score Profile (REST): Create a profile scoped to node_classification = "Person" with weighted dimensions (verification, origin, freshness). 2. Capture (REST): Ingest two Person nodes. One carries rich metadata (verified_time, source = "id-verifier.gov"); the other has only a self-attested source. The metadata is what the dimensions evaluate. 3. Scoring runs on the configured schedule: each Person gets a _TrustScore node attached via _HAS, with property _final_score plus one property per active dimension. The score IS a node in the IKG - every CIQ traversal can see it as subject.trust_score.. 4. CIQ filter (POST /contx-iq/v1/execute): the policy filter requires subject.trust_score._final_score > 0.66 (a fixed threshold baked into the policy). The high-metadata Person clears the bar; the self-attested one does not. Effect: data quality becomes a first-class authorization input - the policy admits a subject only when its computed trust score clears the threshold. ## Use Case Scenario: A KYC-style portal returns a Person's profile only if their underlying identity data clears a freshness/verification bar. The policy hardcodes the threshold at 0.66; the subject is chosen per call via input_params.subject_external_id. Persons captured (relevant metadata only): - Person(knightrider) - email + name carry verified_time = "2026-05-10T08:00:00Z" and source = "id-verifier.gov". - Person(satchmo) - email + name carry source = "user-self-attested" only. After scoring (THREE_HOURS), the IKG has: - (knightrider) -[:_HAS]-> _TrustScore { _final_score: 0.83, verification: 1.0, origin: 0.75, freshness: 0.74 } - (satchmo) -[:_HAS]-> _TrustScore { _final_score: 0.42, verification: 0.0, origin: 0.4, freshness: 0.85 } CIQ executions: - subject_external_id = knightrider -> 0.83 > 0.66 -> returns the profile, including subject.trust_score._final_score = 0.83. - subject_external_id = satchmo -> 0.42 is not > 0.66 -> returns an empty data array. satchmo exists, but the trust threshold isn't met. ## Requirements Prerequisites: - ServiceAccount credentials: For creating the Trust Score Profile, the CIQ policy, and the Knowledge Query. - AppAgent credentials: For ingesting Person nodes via the Capture API and for executing the query. - Bearer token: the subject is a Person, so /contx-iq/v1/execute needs a third-party bearer token alongside X-IK-ClientKey. The subject identity itself comes from input_params.subject_external_id. - Patience: scores are computed on the schedule (THREE_HOURS in this example). The first run produces the initial _TrustScore nodes; subsequent runs update them. Required API access: - POST /configs/v1/trust-score-profiles - POST /capture/v1/nodes - POST /configs/v1/authorization-policies - POST /configs/v1/knowledge-queries - POST /contx-iq/v1/execute ## Steps Step 1: Create the Trust Score Profile for Person - Schedule: THREE_HOURS (allowed values: THREE_HOURS, SIX_HOURS, TWELVE_HOURS, DAILY). Pick longer intervals for stable identity data. - Dimensions: VERIFICATION, ORIGIN, FRESHNESS weighted 1 each; COMPLETENESS and VALIDITY present but weight 0 so they don't influence _final_score. Weights must be between 0 and 1. - node_classification: "Person" - only Person nodes are scored by this profile. Step 2: Capture Persons (with the metadata the profile cares about) - Each scored property should carry the metadata fields the profile uses (property.metadata is a single object: source, verified_time, assurance_level, custom_metadata). - verification dimension reads property.metadata.verified_time. - origin dimension reads property.metadata.source. - freshness dimension uses node + property update times. - Two contrasting Persons make the threshold visible: a high-quality one (DMV-sourced, recently verified) and a self-attested one. - After capture, scoring runs on the profile schedule; each Person gets a _TrustScore node and the value becomes readable as subject.trust_score.* in CIQ. There is no synchronous read - wait for the scheduled pass, then run the query below. Step 3: Create the CIQ Policy - The condition filter ANDs subject.external_id = $subject_external_id with subject.trust_score._final_score > 0.66 (the threshold is fixed in the policy). - allowed_reads exposes the trust score so the response can show why the row was accepted. Step 4: Create and Execute the Knowledge Query - The query simply returns the subject's own fields + subject.trust_score._final_score. - Send input_params.subject_external_id per call (knightrider clears the bar, satchmo does not). - Bearer token + X-IK-ClientKey are required as for any Person-subject CIQ call. ## Code Examples ### Step 1 Trust Score Profile config for Person (flat REST body). node_classification controls which nodes get scored; schedule is one of THREE_HOURS/SIX_HOURS/TWELVE_HOURS/DAILY; each dimensions[].weight is between 0 and 1 (weight = 0 means computed-but-ignored). **POST https://eu.api.indykite.com/configs/v1/trust-score-profiles** ```json { "project_id": "your_project_gid", "name": "person-trust-score-profile", "display_name": "Person Trust Score Profile", "description": "Trust Score profile for Person nodes. Combines verification, origin and freshness with equal weights; completeness and validity are present but weighted 0 (still computed, not used in _final_score).", "node_classification": "Person", "schedule": "THREE_HOURS", "dimensions": [ { "name": "VERIFICATION", "weight": 1 }, { "name": "ORIGIN", "weight": 1 }, { "name": "FRESHNESS", "weight": 1 }, { "name": "COMPLETENESS", "weight": 0 }, { "name": "VALIDITY", "weight": 0 } ] } ``` ### Step 2 Capture two contrasting Person nodes. The metadata object on each property is the actual input to the scoring dimensions - verified_time, source, and update times. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "knightrider", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "knightrider@demo.com", "metadata": { "verified_time": "2026-05-10T08:00:00Z", "source": "id-verifier.gov" } }, { "type": "name", "value": "Michael Knight", "metadata": { "verified_time": "2026-05-10T08:00:00Z", "source": "id-verifier.gov" } } ] }, { "external_id": "satchmo", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "satchmo@demo.com", "metadata": { "source": "user-self-attested" } }, { "type": "name", "value": "Louis Armstrong", "metadata": { "source": "user-self-attested" } } ] } ] } ``` ### Step 3 CIQ Policy. The trust score appears on the subject just like any other node attribute via subject.trust_score._final_score, so it can be ANDed into the regular filter. **policy.json** ```json { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "Person" }, "condition": { "cypher": "MATCH (subject:Person)", "filter": [ { "operator": "AND", "operands": [ { "operator": "=", "attribute": "subject.external_id", "value": "$subject_external_id" }, { "operator": ">", "attribute": "subject.trust_score._final_score", "value": 0.66 } ] } ] }, "allowed_reads": { "nodes": [ "subject.external_id", "subject.property.email", "subject.property.name", "subject.trust_score._final_score" ] } } ``` Request to create the policy. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "CIQ policy that returns the requested Person record ONLY when its computed Trust Score _final_score is above the hardcoded threshold (0.66). The Trust Score Profile must already be active for node_classification = Person; otherwise subject.trust_score._final_score is undefined and the filter rejects every row.", "display_name": "policy - person with trust score filter", "name": "policy-trust-score-filter", "policy": "{\"meta\":{\"policy_version\":\"1.0-ciq\"},\"subject\":{\"type\":\"Person\"},\"condition\":{\"cypher\":\"MATCH (subject:Person)\",\"filter\":[{\"operator\":\"AND\",\"operands\":[{\"operator\":\"=\",\"attribute\":\"subject.external_id\",\"value\":\"$subject_external_id\"},{\"operator\":\">\",\"attribute\":\"subject.trust_score._final_score\",\"value\":0.66}]}]},\"allowed_reads\":{\"nodes\":[\"subject.external_id\",\"subject.property.email\",\"subject.property.name\",\"subject.trust_score._final_score\"]}}", "status": "ACTIVE", "tags": [] } ``` ### Step 4 Knowledge Query - returns the subject's own profile plus subject.trust_score._final_score. **knowledge_query.json** ```json { "nodes": [ "subject.external_id", "subject.property.email", "subject.property.name", "subject.trust_score._final_score" ] } ``` Request to create the Knowledge Query. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "Returns the requested Person's profile but ONLY if their Trust Score _final_score is above the policy threshold (0.66). The trust score itself is included in the response so you can see why a subject was accepted or excluded.", "display_name": "knowledge query - my profile if trustworthy", "name": "kq-my-profile-trusted", "policy_id": "your_policy_gid", "query": "{\"nodes\":[\"subject.external_id\",\"subject.property.email\",\"subject.property.name\",\"subject.trust_score._final_score\"]}", "status": "ACTIVE" } ``` Execute for Knight Rider (high-quality data): input_params.subject_external_id = knightrider. His _final_score 0.83 clears the policy's 0.66 threshold. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "your_query_gid_or_name", "input_params": { "subject_external_id": "knightrider" } } ``` Response: profile returned, _final_score = 0.83 is included as a regular field in the result row. **response_trusted.json** ```json { "data": [ { "nodes": { "subject.external_id": "knightrider", "subject.property.email": "knightrider@demo.com", "subject.property.name": "Michael Knight", "subject.trust_score._final_score": 0.83 } } ] } ``` Execute for Satchmo (self-attested data): input_params.subject_external_id = satchmo. His _final_score 0.42 does not clear the 0.66 threshold. **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "your_query_gid_or_name", "input_params": { "subject_external_id": "satchmo" } } ``` Response: empty data array. The Person exists but the trust score is below the threshold, so the filter drops the row. **response_rejected.json** ```json { "data": [] } ``` ## Common Errors ### 200: data array always empty even for a trustworthy subject **Solution:** The profile may not have run yet, so subject.trust_score._final_score is undefined and the filter rejects every row. With schedule THREE_HOURS the first pass can take up to 3 hours after profile creation - retry the execute after a scoring cycle. ### 200: trust_score._final_score missing from response **Solution:** Add subject.trust_score._final_score (or subject.trust_score.*) to allowed_reads on the policy. The runtime hides any attribute the policy does not declare. ### 400: node_classification does not exist **Solution:** The Trust Score profile is scoped to a specific node label. Use the exact label that the Capture API ingested under (case-sensitive). --- Source: https://developer.indykite.com/resources/ts-1 --- # Trust Score: Read Per-Dimension Scores from a LicenseNumber for Document Vetting > Configure a Trust Score Profile that weights dimensions unevenly for LicenseNumber nodes (validity heaviest, completeness lightest), capture two license numbers with different metadata quality, and run a CIQ query that returns the full per-dimension breakdown - not just the final score - so a vetting UI can show exactly why a document is or isn't trustworthy. **Category:** TrustScore **API:** Trust Score **Tags:** Trust Score, Trust Score Profile, Capture, ContX IQ Policy, ContX IQ Query, Per-Dimension Score, Document Vetting, ln.trust_score.* **Last Updated:** 2026-05-20 **OpenAPI Endpoints:** /configs/v1/trust-score-profiles, /capture/v1/nodes, /capture/v1/relationships, /configs/v1/authorization-policies, /configs/v1/knowledge-queries, /contx-iq/v1/execute **Related Guides:** /guides/guide-trust-score, /guides/guide-contx-iq ## Summary Trust Score doesn't have to be a single number. Each dimension is a separately-readable property on the _TrustScore node, so a CIQ query can return the whole breakdown. Pipeline: 1. Trust Score Profile (REST): node_classification = "LicenseNumber". Weights are uneven (each between 0 and 1) - validity = 1, verification = 0.6, freshness = 0.3, origin = 0.3, completeness = 0.3 - because an expired or unsourced licence should drag the final score down hard. Schedule is THREE_HOURS (allowed: THREE_HOURS, SIX_HOURS, TWELVE_HOURS, DAILY). 2. Capture (REST): Ingest the Person (alice), her Cars, and two LicenseNumber nodes. Each LicenseNumber carries: - issued_on and valid_until as plain properties (string values) -> feed the validity dimension. - a number property whose metadata object carries the documented capture.Metadata fields verified_time -> verification and source -> origin. - freshness uses node + property update times; completeness reflects how fully the node is populated. 3. After scoring, each LicenseNumber has a _TrustScore node with both _final_score and one property per configured dimension. Scoring is asynchronous: it runs on the profile schedule (THREE_HOURS minimum) and there is no REST endpoint to trigger it on demand. Until the first pass completes, ln.trust_score.* reads as null. 4. CIQ read (POST /contx-iq/v1/execute): the policy's allowed_reads lists ln.trust_score.*, so the Knowledge Query can return _final_score AND every dimension. The UI uses those for the per-dimension bars - once scoring has run. Result: the same node carries both the IKG facts (number, issued_on, ...) and the computed trustworthiness - visible side-by-side in one response. ## Use Case Scenario: A claims-processing console renders one row per car license number with a trust bar broken into "validity / verification / freshness / origin / completeness" colored cells. Caseworkers spot at a glance why a low score is low - a 1.0 validity but 0.5 verification is a different problem from 0.0 validity (expired). Captured license numbers: - LicenseNumber(ln-kitt-0001) - issued_on 2025-09-01, valid_until 2030-09-01 (properties); number.metadata.verified_time 2026-04-12, source "state-dmv". Recently issued, currently valid, verified by an official authority. - LicenseNumber(ln-cad-007) - issued_on 2018-02-15, valid_until 2024-02-15 (properties); number.metadata.source "user-self-attested", no verified_time. Already expired, never independently verified. After the next scheduled pass: - ln-kitt-0001 _TrustScore: { _final_score: 0.91, validity: 1.0, verification: 1.0, freshness: 0.92, origin: 1.0, completeness: 0.6 } - ln-cad-007 _TrustScore: { _final_score: 0.34, validity: 0.0, verification: 0.5, freshness: 0.41, origin: 0.3, completeness: 0.5 } The CIQ query returns both rows in one execute call so the UI can sort, filter, or page through them with no client-side combination needed. ## Requirements Prerequisites: - ServiceAccount credentials: For the Trust Score Profile, policy, and Knowledge Query. - AppAgent credentials: For Capture and CIQ execute. - Bearer token: the subject is a Person, so /contx-iq/v1/execute needs a third-party bearer token alongside X-IK-ClientKey. The subject identity comes from input_params.subject_external_id. - This example captures its own Person -[OWNS]-> Car -[HAS]-> LicenseNumber sub-graph (alice owns kitt and caddilacv16) so the caseworker can traverse to the documents under review. Required API access: - POST /configs/v1/trust-score-profiles - 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 LicenseNumber Trust Score Profile - Schedule: THREE_HOURS (allowed values: THREE_HOURS, SIX_HOURS, TWELVE_HOURS, DAILY). - Weights (each between 0 and 1): validity 1 dominates _final_score; verification 0.6 is the next strongest signal; freshness, origin, completeness each weight 0.3. Adjust the ratios to change behavior. Step 2: Capture the Graph - Ingest Person (alice), her Cars, and two LicenseNumber nodes, plus OWNS and HAS relationships. - issued_on and valid_until are plain properties; the number property's metadata object carries the documented fields verified_time and source. Capture the same fields whether the documents are "good" or "bad" - let the dimensions render the difference. Step 3: Create the CIQ Policy - subject = Person (filtered by $subject_external_id), traversal goes Person -[OWNS]-> Car -[HAS]-> LicenseNumber. - allowed_reads uses the wildcard ln.trust_score.* so every dimension property is exposed without naming each one. Step 4: Create the Knowledge Query - Pull car.external_id, ln.property.number, ln.trust_score._final_score, and each dimension explicitly. (You can also write ln.trust_score.* in the policy and pick fields one by one in the Knowledge Query.) Step 5: Execute (note the async timing) - Pass input_params.subject_external_id (alice). The traversal returns one row per license number immediately, but ln.trust_score.* will be null until the profile's first scoring pass has run. Trust scoring is asynchronous and schedule-driven (THREE_HOURS minimum); there is no REST endpoint to trigger it on demand. Step 6: Confirm scoring has run - GET /configs/v1/trust-score-profiles/{id} and check last_run_id / last_run_start_time / last_run_end_time. Empty last_run_id means scoring hasn't happened yet. Step 7: Re-execute and Render - After the scheduled pass, the SAME execute returns populated ln.trust_score.* values. Render the bars from the per-dimension values; sort or filter by _final_score. ## Code Examples ### Step 1 Trust Score Profile for LicenseNumber (flat REST body). Validity weight 1.0 is the heaviest because an expired licence should fail the vetting even if everything else is rich; other dimensions are weighted lower (all between 0 and 1). schedule is one of THREE_HOURS/SIX_HOURS/TWELVE_HOURS/DAILY. **POST https://eu.api.indykite.com/configs/v1/trust-score-profiles** ```json { "project_id": "your_project_gid", "name": "licensenumber-vetting-profile", "display_name": "LicenseNumber Vetting Profile", "description": "Trust Score profile for LicenseNumber nodes used in document vetting. Validity carries the heaviest weight (1.0) because an expired licence number should drag the final score down hard even if the rest of the metadata is rich. Weights must be between 0 and 1.", "node_classification": "LicenseNumber", "schedule": "THREE_HOURS", "dimensions": [ { "name": "VALIDITY", "weight": 1 }, { "name": "VERIFICATION", "weight": 0.6 }, { "name": "FRESHNESS", "weight": 0.3 }, { "name": "ORIGIN", "weight": 0.3 }, { "name": "COMPLETENESS", "weight": 0.3 } ] } ``` ### Step 2 Capture Person (alice), her Cars, and two LicenseNumber nodes. issued_on/valid_until are plain properties (feed the validity dimension); the number property's metadata object carries verified_time (verification) and source (origin) - the documented capture.Metadata fields. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "alice", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "alice@email.com" } ] }, { "external_id": "kitt", "type": "Car", "properties": [ { "type": "model", "value": "Firebird" } ] }, { "external_id": "caddilacv16", "type": "Car", "properties": [ { "type": "model", "value": "V-16" } ] }, { "external_id": "ln-kitt-0001", "type": "LicenseNumber", "properties": [ { "type": "number", "value": "KITT 0001", "metadata": { "verified_time": "2026-04-12T10:00:00Z", "source": "state-dmv" } }, { "type": "issued_on", "value": "2025-09-01" }, { "type": "valid_until", "value": "2030-09-01" } ] }, { "external_id": "ln-cad-007", "type": "LicenseNumber", "properties": [ { "type": "number", "value": "CADV16-007", "metadata": { "source": "user-self-attested" } }, { "type": "issued_on", "value": "2018-02-15" }, { "type": "valid_until", "value": "2024-02-15" } ] } ] } ``` Capture the relationships: alice OWNS each Car, and each Car HAS its LicenseNumber - the path the policy traverses. **POST https://eu.api.indykite.com/capture/v1/relationships/** ```json { "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": "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" } ] } ``` ### Step 3 CIQ Policy. allowed_reads exposes ln.trust_score.* - wildcard means every property on the attached _TrustScore node is readable, including ones added later when the profile gains a new dimension. **policy.json** ```json { "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_reads": { "nodes": [ "car.external_id", "ln.property.number", "ln.trust_score.*" ] } } ``` Request to create the policy. **POST https://eu.api.indykite.com/configs/v1/authorization-policies** ```json { "project_id": "your_project_gid", "description": "CIQ policy used by a document-vetting console. The subject (signed-in caseworker) walks Person -> OWNS -> Car -> HAS -> LicenseNumber, and the policy exposes the full _TrustScore breakdown via ln.trust_score.* so the UI can render per-dimension bars (validity / verification / freshness / origin / completeness) alongside the final score.", "display_name": "policy - license number per-dimension trust score", "name": "policy-licensenumber-trust-dimensions", "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_reads\":{\"nodes\":[\"car.external_id\",\"ln.property.number\",\"ln.trust_score.*\"]}}", "status": "ACTIVE", "tags": [] } ``` ### Step 4 Knowledge Query. Each dimension is enumerated explicitly so the UI can be sure each field is present in the response shape; alternatively, the same query works with ln.trust_score.* if the UI tolerates a variable set of keys. **knowledge_query.json** ```json { "nodes": [ "car.external_id", "ln.property.number", "ln.trust_score._final_score", "ln.trust_score.validity", "ln.trust_score.verification", "ln.trust_score.freshness", "ln.trust_score.origin", "ln.trust_score.completeness" ] } ``` Request to create the Knowledge Query. **POST https://eu.api.indykite.com/configs/v1/knowledge-queries** ```json { "project_id": "your_project_gid", "description": "Document-vetting query. Returns the caller's cars, their license numbers, and the full Trust Score breakdown of each license number — final_score plus the five dimensions captured by the LicenseNumber Vetting Profile.", "display_name": "knowledge query - license number trust score breakdown", "name": "kq-licensenumber-trust-breakdown", "policy_id": "your_policy_gid", "query": "{\"nodes\":[\"car.external_id\",\"ln.property.number\",\"ln.trust_score._final_score\",\"ln.trust_score.validity\",\"ln.trust_score.verification\",\"ln.trust_score.freshness\",\"ln.trust_score.origin\",\"ln.trust_score.completeness\"]}", "status": "ACTIVE" } ``` ### Step 5 Execute the query with input_params.subject_external_id = alice (plus X-IK-ClientKey and a bearer token, since the subject is a Person). **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "id": "your_query_gid_or_name", "input_params": { "subject_external_id": "alice" } } ``` Immediate response - BEFORE the first scoring run. The graph traversal works (both rows return), but every ln.trust_score.* is null because no _TrustScore node has been computed yet. Trust scoring is asynchronous: it runs on the profile schedule (THREE_HOURS minimum), and there is no REST endpoint to trigger it on demand. **response_before_scoring.json** ```json { "data": [ { "nodes": { "car.external_id": "caddilacv16", "ln.property.number": "CADV16-007", "ln.trust_score._final_score": null, "ln.trust_score.validity": null, "ln.trust_score.verification": null, "ln.trust_score.freshness": null, "ln.trust_score.origin": null, "ln.trust_score.completeness": null } }, { "nodes": { "car.external_id": "kitt", "ln.property.number": "KITT 0001", "ln.trust_score._final_score": null, "ln.trust_score.validity": null, "ln.trust_score.verification": null, "ln.trust_score.freshness": null, "ln.trust_score.origin": null, "ln.trust_score.completeness": null } } ] } ``` ### Step 6 Confirm scoring has run before expecting values: read the profile and check last_run_id / last_run_start_time / last_run_end_time. Empty last_run_id means the first pass hasn't happened yet. **GET https://eu.api.indykite.com/configs/v1/trust-score-profiles/{id}** ```json { "id": "your_trust_score_profile_gid" } ``` Profile read response: last_run_* are populated once a scoring pass has completed. **response.json** ```json { "id": "gid:AAAAExampleTrustScoreProfile", "name": "licensenumber-vetting-profile", "node_classification": "LicenseNumber", "schedule": "THREE_HOURS", "dimensions": [ { "name": "VALIDITY", "weight": 1 }, { "name": "VERIFICATION", "weight": 0.6 }, { "name": "FRESHNESS", "weight": 0.3 }, { "name": "ORIGIN", "weight": 0.3 }, { "name": "COMPLETENESS", "weight": 0.3 } ], "last_run_id": "gid:AAAAExampleLastRun", "last_run_start_time": "2026-05-20T09:00:00Z", "last_run_end_time": "2026-05-20T09:00:12Z" } ``` ### Step 7 Re-run the SAME execute AFTER the scheduled scoring pass. Now ln.trust_score.* is populated: ln-kitt-0001 scores high; ln-cad-007 is dragged down by validity = 0 (expired) and origin = 0.3 (self-attested only). **POST https://eu.api.indykite.com/contx-iq/v1/execute** ```json { "data": [ { "nodes": { "car.external_id": "kitt", "ln.property.number": "KITT 0001", "ln.trust_score._final_score": 0.91, "ln.trust_score.validity": 1, "ln.trust_score.verification": 1, "ln.trust_score.freshness": 0.92, "ln.trust_score.origin": 1, "ln.trust_score.completeness": 0.6 } }, { "nodes": { "car.external_id": "caddilacv16", "ln.property.number": "CADV16-007", "ln.trust_score._final_score": 0.34, "ln.trust_score.validity": 0, "ln.trust_score.verification": 0.5, "ln.trust_score.freshness": 0.41, "ln.trust_score.origin": 0.3, "ln.trust_score.completeness": 0.5 } } ] } ``` ## Common Errors ### 200: ln.trust_score.* values are all null **Solution:** Scoring hasn't run yet. Trust scores are computed asynchronously on the profile schedule (THREE_HOURS minimum) and there is no on-demand trigger in the REST API. Check GET /configs/v1/trust-score-profiles/{id} - if last_run_id is empty the first pass hasn't happened. Re-run the execute after a scoring cycle and the values populate. ### 400: allowed_reads missing trust_score attribute **Solution:** Add ln.trust_score.* (wildcard) or the specific dimensions (ln.trust_score.validity etc.) to allowed_reads. The runtime hides any attribute the policy does not declare. ### 404: no LicenseNumber matched the caller **Solution:** The CIQ traversal Person -[OWNS]-> Car -[HAS]-> LicenseNumber requires the captured graph to wire each LicenseNumber to a Car the caller owns. Confirm relationships exist or relax the cypher for a public read. --- Source: https://developer.indykite.com/resources/ts-2 --- # Token Introspect configuration > Create a Token Introspect configuration for a Person node with email claim. **Category:** Token Introspect **Last Updated:** 2026-03-20 **Terraform Provider Docs:** https://registry.terraform.io/providers/indykite/indykite/latest/docs/resources/token_introspect **Related Guides:** /guides/guide-terraform, /guides/guide-environment ## Summary Token Introspect validates access tokens and retrieves associated identity data from your Identity Knowledge Graph (IKG). This Terraform configuration creates a Token Introspect setup that: 1. Accepts an access token containing an email claim. 2. Matches the email claim to a Person node in the IKG. 3. Returns the Person node data if a match is found. ## Use Case Scenario: Your application receives access tokens from an identity provider and needs to enrich them with IKG data. When a user authenticates, the Token Introspect configuration matches the token's email claim to a Person node, allowing your application to access identity attributes and relationships stored in the graph. ## Requirements - ServiceAccount credentials created in the IndyKite Hub for your organization. - A Person node with an email property must exist in the IKG for matching to succeed. ## Steps 1. Create the Token Introspect configuration using the Terraform file below. 2. Apply the Terraform configuration to your IndyKite project. 3. Call the Token Introspect endpoint with an access token containing an email claim. 4. Receive the matched Person node data in the response. ## Code Examples Create a Token Introspect configuration for a Person node with email claim. **main.tf** ```hcl terraform { required_providers { indykite = { source = "indykite/indykite" version = 1.34. // or latest version } } } # indykite provider integrates IndyKite platform with Terraform scripting. # Provider for now does not support any parameters and all is set within service account credential file. provider "indykite" {} resource "indykite_token_introspect" "token_config" { name = "terraform-token-introspect" display_name = "Terraform token introspect" description = "Token introspect for DigitalTwin access token" location = "ProjectGID" jwt_matcher { issuer = "https://example.com" audience = "client-id" } offline_validation {} ikg_node_type = "Token" claims_mapping = { "email" = "email" } perform_upsert = true } ``` ## Common Errors ### INVALID_ARGUMENT: Invalid token_matcher configuration **Solution:** Ensure the token_matcher references a valid node type and property path ### NOT_FOUND: Application agent not found **Solution:** Verify the app_agent_id references an existing application agent --- Source: https://developer.indykite.com/terraform/terraform-1 --- # Environment configuration > Create a project, an application, an application agent, an application agent credential to get an Application token. **Category:** Environment **Last Updated:** 2026-03-20 **Terraform Provider Docs:** https://registry.terraform.io/providers/indykite/indykite/latest/docs/resources/application_space, https://registry.terraform.io/providers/indykite/indykite/latest/docs/resources/application, https://registry.terraform.io/providers/indykite/indykite/latest/docs/resources/application_agent, https://registry.terraform.io/providers/indykite/indykite/latest/docs/resources/application_agent_credential **Related Guides:** /guides/guide-terraform, /guides/guide-environment, /guides/guide-credentials ## Summary This Terraform configuration sets up a complete IndyKite environment hierarchy: 1. Project: Container for applications and configurations. 2. Application: Represents your software system. 3. Application Agent: Identity that authenticates API calls. 4. Application Agent Credential: Token used for API authentication. The configuration also creates an Identity Knowledge Graph (IKG) with an _Application node. ## Use Case Scenario: You need to integrate your application with the IndyKite platform. Before calling any IndyKite API (Capture, Query, Authorization), your application needs valid credentials. This configuration creates the required hierarchy and outputs an Application Agent token. The token can be used as: - Bearer token for REST API calls. - API Key for SDK authentication. Note: The Config API requires ServiceAccount credentials at the organization level, not Application Agent credentials. ## Requirements - ServiceAccount credentials created in the IndyKite Hub for your organization. - Terraform CLI installed on your machine. ## Steps 1. Configure the Terraform provider with your ServiceAccount credentials. 2. Apply the Terraform configuration to create the environment hierarchy. 3. Retrieve the Application Agent credential from the Terraform output. 4. Use the credential as Bearer token or API Key in your application. ## Code Examples **main.tf** ```hcl terraform { required_providers { indykite = { source = "indykite/indykite" version = 1.34. # or latest version } } } # indykite provider integrates IndyKite platform with Terraform scripting. # Provider for now does not support any parameters and all is set within service account credential file. provider "indykite" {} # call the indykite_customer datasource data "indykite_customer" "customer1" { name = "your-customer-name" } # call the indykite_application_space resource to create a new project resource "indykite_application_space" "appspace1" { customer_id = data.indykite_customer.customer.id name = "project-name" display_name = "Prject display name" description = "Description of your project" region = "europe-west1" # or us-east1 ikg_size = "4GB" # default 2GB } # call the indykite_application resource to create a new application resource "indykite_application" "application1" { app_space_id = indykite_application_space.appspace.id name = "application-name" display_name = "Application display name" description = "Description of your application" } # call the indykite_application_agent to create a new application agent resource "indykite_application_agent" "agent" { application_id = indykite_application.application.id name = "application-agent-name" display_name = "Application agent display name" description = "Description of your application agent" } # call the indykite_application_agent_credential to create a new application agent credential resource "indykite_application_agent_credential" "with_public" { app_agent_id = indykite_application_agent.agent.id display_name = "Credential display name" expire_time = "2026-12-31T12:34:56-01:00" #must be less than 2 years to generate a token } ``` ## Common Errors ### ALREADY_EXISTS: Resource with this name already exists **Solution:** Use a unique name or import the existing resource into Terraform state ### PERMISSION_DENIED: Insufficient permissions **Solution:** Ensure ServiceAccount credentials have organization-level permissions --- Source: https://developer.indykite.com/terraform/terraform-2 --- # Kafka Outbound Events / Signal - Config > Create an Outbound Events configuration with a Kafka provider. **Category:** Outbound Events **Last Updated:** 2026-03-20 **Terraform Provider Docs:** https://registry.terraform.io/providers/indykite/indykite/latest/docs/resources/event_sink **Related Guides:** /guides/guide-terraform, /guides/guide-outbound-events ## Summary Outbound Events (Signals) push real-time notifications to external systems when changes occur in your IndyKite environment. This configuration sets up event streaming to Kafka (Confluent) for configuration changes: 1. Define Confluent as the event provider. 2. Route configuration change events to a Kafka topic. 3. Receive messages when any configuration is created, read, updated, or deleted. ## Use Case Scenario: You need to audit or react to configuration changes in your IndyKite project. Each time a configuration node is modified (create, read, update, delete), an event is sent to your Kafka topic. This enables: - Audit logging of all configuration changes. - Triggering downstream workflows when configurations are updated. - Real-time monitoring of your IndyKite environment. Note: Only one Outbound Events configuration can be active per project. ## Requirements - ServiceAccount credentials created in the IndyKite Hub for your organization. - A Confluent Cloud environment with a valid API Key. - A Kafka topic created in your Confluent environment. ## Steps 1. Create a topic in your Confluent environment to receive events. 2. Apply the Terraform configuration to create the Outbound Events setup. 3. Perform any CRUD action on a configuration node in your project. 4. Verify that event messages appear in your Kafka topic. ## Code Examples **main.tf** ```hcl terraform { required_providers { indykite = { source = "indykite/indykite" version = 1.34. # or latest version } } } # indykite provider integrates IndyKite platform with Terraform scripting. # Provider for now does not support any parameters and all is set within service account credential file. provider "indykite" {} resource "time_static" "example" {} resource "indykite_event_sink" "outbound_events" { name = "outbound-events" display_name = "Outbound Events" location = "project_id" providers { provider_name = "confluent-provider" include_cdc_events = false kafka { brokers = ["broker"] topic = "topic_signal" username = "api_key" password = "api_key_secret" } } routes { provider_id = "confluent-provider" stop_processing = true keys_values_filter { event_type = "indykite.audit.config.*" } route_display_name = "Configuration Audit Events" route_id = "config-audit-log" } # lifecycle { # create_before_destroy = true # } } resource "indykite_authorization_policy" "policy_drive_car" { name = "terraform-policy-drive-car" display_name = "Terraform policy drive car" description = "Policy person drive car" json = jsonencode({ meta = { policy_version = "2.0-kbac" }, subject = { type = "Person" }, actions = ["CAN_DRIVE"], resource = { type = "Car" }, condition = { cypher = "MATCH (subject)-[:DRIVES]->(resource:Car)" } }) location = "project_id" status = "active" } ``` ## Common Errors ### ALREADY_EXISTS: Only one event sink per project **Solution:** Delete the existing event sink before creating a new one, or import it into Terraform state ### INVALID_ARGUMENT: Invalid Kafka configuration **Solution:** Verify Confluent API key, secret, and bootstrap server URL are correct --- Source: https://developer.indykite.com/terraform/terraform-3 --- # Kafka Outbound Events / Signal - Capture > Create an Outbound Events configuration with a Kafka provider to stream data capture events. **Category:** Outbound Events **Last Updated:** 2026-03-20 **Terraform Provider Docs:** https://registry.terraform.io/providers/indykite/indykite/latest/docs/resources/event_sink **Related Guides:** /guides/guide-terraform, /guides/guide-outbound-events ## Summary Outbound Events (Signals) push real-time notifications when nodes or relationships are captured in your IKG. This configuration sets up event streaming to Kafka (Confluent) for data capture events: 1. Define Confluent as the event provider. 2. Route capture events matching specific criteria to a Kafka topic. 3. Filter events by node label and property values (e.g., Car nodes where manufacturer = "pontiac"). ## Use Case Scenario: You need to react when specific data is captured in your IKG. Each time a node matching your criteria is created, updated, or deleted, an event is sent to your Kafka topic. This enables: - Real-time data synchronization with external systems. - Triggering workflows when specific node types are modified. - Selective event filtering to reduce noise and processing overhead. Note: Only one Outbound Events configuration can be active per project. ## Requirements - ServiceAccount credentials created in the IndyKite Hub for your organization. - Application Agent credentials for capturing data via the Capture API. - A Confluent Cloud environment with a valid API Key. - A Kafka topic created in your Confluent environment. ## Steps 1. Create a topic in your Confluent environment to receive events. 2. Apply the Terraform configuration with indykite.audit.capture.* eventType and filter criteria. 3. Capture nodes with the Car label and manufacturer property set to "pontiac". 4. Add more nodes matching the same criteria. 5. Verify that event messages appear in your Kafka topic for matching nodes. 6. Confirm that nodes with different properties do not trigger events. ## Code Examples ### Step 2 **main.tf** ```hcl terraform { required_providers { indykite = { source = "indykite/indykite" version = 1.34. # or latest version } } } # indykite provider integrates IndyKite platform with Terraform scripting. # Provider for now does not support any parameters and all is set within service account credential file. provider "indykite" {} resource "time_static" "example" {} resource "indykite_event_sink" "outbound_event" { name = "outbound-event" display_name = "Outbound Event" location = "project_id" providers { provider_name = "provider-name" include_cdc_events = false kafka { brokers = ["broker"] topic = "topic_signal" username = "api_key" password = "api_secret" } } routes { provider_id = "provider-name" stop_processing = true keys_values_filter { key_value_pairs { key = "manufacturer" value = "pontiac" } key_value_pairs { key = "captureLabel" value = "Car" } event_type = "indykite.audit.capture.upsert.node" } route_display_name = "Configuration Audit Events" route_id = "config-audit-log" } } ``` ### Step 3 Capture the nodes needed for this use case. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "alice", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "alice@email.com" }, { "type": "given_name", "value": "Alice" }, { "type": "last_name", "value": "Smith" } ] }, { "external_id": "knightrider", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "knightrider@demo.com" }, { "type": "name", "value": "Michael Knight" } ] }, { "external_id": "satchmo", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "satchmo@demo.com" }, { "type": "name", "value": "Louis Armstrong" } ] }, { "external_id": "karel", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "karel@demo.com" }, { "type": "name", "value": "Karel Plihal" } ] }, { "external_id": "kitt", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "pontiac" }, { "type": "model", "value": "Firebird" } ] }, { "external_id": "cadillacv16", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "Cadillac" }, { "type": "model", "value": "V-16" } ] }, { "external_id": "harmonika", "type": "Bus", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "Ikarus" }, { "type": "model", "value": "280" } ] }, { "external_id": "listek", "type": "Ticket", "is_identity": false }, { "external_id": "airbook-xyz", "type": "Laptop", "is_identity": false } ] } ``` ### Step 4 Capture additional nodes to trigger more events. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "kitten", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "pontiac" }, { "type": "model", "value": "Bonneville" } ] }, { "external_id": "kitty", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "pontiac" }, { "type": "model", "value": "Catalina" } ] } ] } ``` ## Common Errors ### ALREADY_EXISTS: Only one event sink per project **Solution:** Delete the existing event sink before creating a new one ### INVALID_ARGUMENT: Invalid filter expression **Solution:** Check that the label and property filters match your IKG schema --- Source: https://developer.indykite.com/terraform/terraform-4 --- # Azure Event Grid Outbound Events / Signal - Capture > Create an Outbound Events configuration with Azure Event Grid to stream batch upsert events. **Category:** Outbound Events **Last Updated:** 2026-07-15 **Terraform Provider Docs:** https://registry.terraform.io/providers/indykite/indykite/latest/docs/resources/event_sink **Related Guides:** /guides/guide-terraform, /guides/guide-outbound-events ## Summary Outbound Events (Signals) push real-time notifications when nodes are batch-upserted in your IKG. This configuration sets up event streaming to Azure Event Grid: 1. Define Azure Event Grid as the event provider. 2. Route BatchUpsertNodes events matching specific criteria to your topic. 3. Filter events by node label and property values (e.g., Car nodes where manufacturer = "pontiac"). ## Use Case Scenario: You need to react when specific data is batch-upserted in your IKG via the REST API. Each time nodes matching your criteria are upserted via the BatchUpsertNodes endpoint, an event is sent to your Azure Event Grid topic. This enables: - Real-time data synchronization with Azure services. - Triggering Azure Functions or Logic Apps when specific data arrives. - Selective event filtering to process only relevant batch operations. Note: Only one Outbound Events configuration can be active per project. This configuration specifically targets the indykite.audit.capture.upsert.node event type. ## Requirements - ServiceAccount credentials created in the IndyKite Hub for your organization. - Application Agent credentials for capturing data via the Capture API. - An Azure Event Grid namespace with a topic and subscription ready. ## Steps 1. Ensure your Azure Event Grid topic and subscription are configured. 2. Apply the Terraform configuration with event_type set to indykite.audit.capture.upsert.node. 3. Capture nodes with the Car label and manufacturer property set to "pontiac". 4. Add more nodes matching the same criteria. 5. Verify that event messages appear in your Azure Event Grid topic. 6. Confirm that non-matching nodes and other event types do not trigger events. ## Code Examples ### Step 2 **main.tf** ```hcl terraform { required_providers { indykite = { source = "indykite/indykite" version = 1.34. # or latest version } } } # indykite provider integrates IndyKite platform with Terraform scripting. # Provider for now does not support any parameters and all is set within service account credential file. provider "indykite" {} resource "time_static" "example" {} resource "indykite_event_sink" "outbound_event" { name = "outbound-event" display_name = "Outbound Event" location = "project_id" providers { provider_name = "provider-name" include_cdc_events = false azure_event_grid { topic_endpoint = "https://ik-test.eventgrid.azure.net/api/events" access_key = "secret-access-key" } } routes { provider_id = "provider-name" stop_processing = true keys_values_filter { key_value_pairs { key = "manufacturer" value = "pontiac" } key_value_pairs { key = "captureLabel" value = "Car" } event_type = "indykite.audit.capture.upsert.node" } route_display_name = "Configuration Audit Events" route_id = "config-audit-log" } } ``` ### Step 3 Capture the nodes needed for this use case. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "alice", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "alice@email.com" }, { "type": "given_name", "value": "Alice" }, { "type": "last_name", "value": "Smith" } ] }, { "external_id": "knightrider", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "knightrider@demo.com" }, { "type": "name", "value": "Michael Knight" } ] }, { "external_id": "satchmo", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "satchmo@demo.com" }, { "type": "name", "value": "Louis Armstrong" } ] }, { "external_id": "karel", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "karel@demo.com" }, { "type": "name", "value": "Karel Plihal" } ] }, { "external_id": "kitt", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "pontiac" }, { "type": "model", "value": "Firebird" } ] }, { "external_id": "cadillacv16", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "Cadillac" }, { "type": "model", "value": "V-16" } ] }, { "external_id": "harmonika", "type": "Bus", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "Ikarus" }, { "type": "model", "value": "280" } ] }, { "external_id": "listek", "type": "Ticket", "is_identity": false }, { "external_id": "airbook-xyz", "type": "Laptop", "is_identity": false } ] } ``` ### Step 4 Capture additional nodes to trigger more events. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "kitten", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "pontiac" }, { "type": "model", "value": "Bonneville" } ] }, { "external_id": "kitty", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "pontiac" }, { "type": "model", "value": "Catalina" } ] } ] } ``` ## Common Errors ### ALREADY_EXISTS: Only one event sink per project **Solution:** Delete the existing event sink before creating a new one ### INVALID_ARGUMENT: Invalid Azure Event Grid configuration **Solution:** Verify the Event Grid namespace URL and access key are correct --- Source: https://developer.indykite.com/terraform/terraform-5 --- # Azure Service Bus Outbound Events / Signal - Capture > Create an Outbound Events configuration with Azure Service Bus to stream capture events. **Category:** Outbound Events **Last Updated:** 2026-03-20 **Terraform Provider Docs:** https://registry.terraform.io/providers/indykite/indykite/latest/docs/resources/event_sink **Related Guides:** /guides/guide-terraform, /guides/guide-outbound-events ## Summary Outbound Events (Signals) push real-time notifications when nodes are captured in your IKG. This configuration sets up event streaming to Azure Service Bus: 1. Define Azure Service Bus as the event provider. 2. Route all capture events (upsert and delete) matching specific criteria to your topic. 3. Filter events by node label and property values (e.g., Car nodes where manufacturer = "pontiac"). ## Use Case Scenario: You need to react when specific data is captured or deleted in your IKG. Each time a node matching your criteria is created, updated, or deleted, an event is sent to your Azure Service Bus topic. This enables: - Real-time data synchronization with Azure services. - Triggering Azure Functions, Logic Apps, or other Service Bus consumers. - Reliable message delivery with Service Bus queuing capabilities. Note: Only one Outbound Events configuration can be active per project. The wildcard indykite.audit.capture.* matches all capture event types. ## Requirements - ServiceAccount credentials created in the IndyKite Hub for your organization. - Application Agent credentials for capturing data via the Capture API. - An Azure Service Bus namespace with a topic and subscription configured. ## Steps 1. Ensure your Azure Service Bus topic and subscription are configured. 2. Apply the Terraform configuration with indykite.audit.capture.* eventType. 3. Capture nodes with the Car label and manufacturer property set to "pontiac". 4. Add more nodes matching the same criteria. 5. Verify that event messages appear in your Azure Service Bus topic. 6. Confirm that non-matching nodes do not trigger events. ## Code Examples ### Step 2 **main.tf** ```hcl terraform { required_providers { indykite = { source = "indykite/indykite" version = 1.34. # or latest version } } } # indykite provider integrates IndyKite platform with Terraform scripting. # Provider for now does not support any parameters and all is set within service account credential file. provider "indykite" {} resource "time_static" "example" {} resource "indykite_event_sink" "outbound_event" { name = "outbound-event" display_name = "Outbound Event" location = "project_id" providers { provider_name = "provider-name" include_cdc_events = false azure_service_bus { connection_string = "Endpoint=sb://ik-test.servicebus.windows.net/;SharedAccessKeyName=xxxxx;SharedAccessKey=xxxxxxx" queue_or_topic_name = "capture-changes" } } routes { provider_id = "provider-name" stop_processing = true keys_values_filter { key_value_pairs { key = "manufacturer" value = "pontiac" } key_value_pairs { key = "captureLabel" value = "Car" } event_type = "indykite.audit.capture.upsert.node" } route_display_name = "Configuration Audit Events" route_id = "config-audit-log" } } ``` ### Step 3 Capture the nodes needed for this use case. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "alice", "is_identity": true, "type": "Person", "properties": [ { "type": "email", "value": "alice@email.com" }, { "type": "given_name", "value": "Alice" }, { "type": "last_name", "value": "Smith" } ] }, { "external_id": "knightrider", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "knightrider@demo.com" }, { "type": "name", "value": "Michael Knight" } ] }, { "external_id": "satchmo", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "satchmo@demo.com" }, { "type": "name", "value": "Louis Armstrong" } ] }, { "external_id": "karel", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "karel@demo.com" }, { "type": "name", "value": "Karel Plihal" } ] }, { "external_id": "kitt", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "pontiac" }, { "type": "model", "value": "Firebird" } ] }, { "external_id": "cadillacv16", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "Cadillac" }, { "type": "model", "value": "V-16" } ] }, { "external_id": "harmonika", "type": "Bus", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "Ikarus" }, { "type": "model", "value": "280" } ] }, { "external_id": "listek", "type": "Ticket", "is_identity": false }, { "external_id": "airbook-xyz", "type": "Laptop", "is_identity": false } ] } ``` ### Step 4 Capture additional nodes to trigger more events. **POST https://eu.api.indykite.com/capture/v1/nodes/** ```json { "nodes": [ { "external_id": "kitten", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "pontiac" }, { "type": "model", "value": "Bonneville" } ] }, { "external_id": "kitty", "type": "Car", "is_identity": false, "properties": [ { "type": "manufacturer", "value": "pontiac" }, { "type": "model", "value": "Catalina" } ] } ] } ``` ## Common Errors ### ALREADY_EXISTS: Only one event sink per project **Solution:** Delete the existing event sink before creating a new one ### INVALID_ARGUMENT: Invalid Azure Service Bus configuration **Solution:** Verify the Service Bus namespace URL and shared access key are correct --- Source: https://developer.indykite.com/terraform/terraform-6 --- # Pub/Sub Outbound Events / Signal - Config > Create an Outbound Events configuration with a Pub/Sub provider. **Category:** Outbound Events **Last Updated:** 2026-04-28 **Terraform Provider Docs:** https://registry.terraform.io/providers/indykite/indykite/latest/docs/resources/event_sink **Related Guides:** /guides/guide-terraform, /guides/guide-outbound-events ## Summary Outbound Events (Signals) push real-time notifications to external systems when changes occur in your IndyKite environment. This configuration sets up event streaming to Pub/Sub for configuration changes: 1. Define Pub/Sub as the event provider. 2. Route configuration change events to a Pub/Sub topic. 3. Receive messages when any configuration is created, read, updated, or deleted. ## Use Case Scenario: You need to audit or react to configuration changes in your IndyKite project. Each time a configuration node is modified (create, read, update, delete), an event is sent to your Pub/Sub topic. This enables: - Audit logging of all configuration changes. - Triggering downstream workflows when configurations are updated. - Real-time monitoring of your IndyKite environment. Note: Only one Outbound Events configuration can be active per project. ## Requirements - ServiceAccount credentials created in the IndyKite Hub for your organization. - A GCP Service Account with a valid API Key with IAM permission roles/pubsub.editor (or roles/pubsub.publisher + roles/pubsub.viewer). - A Pub/Sub topic created in your GCP Project. - A subscription for the topic ## Steps 1. Securely provide GCP Service Account credentials to the Terraform configuration via a credentials file. 2. Apply the Terraform configuration to create the Outbound Events setup. 3. Perform any CRUD action on a configuration node in your project. 4. Verify that event messages appear in your Pub/Sub topic subscription. ## Code Examples **main.tf** ```hcl terraform { required_providers { indykite = { source = "indykite/indykite" version = 1.34. # or latest version } } } # indykite provider integrates IndyKite platform with Terraform scripting. # Provider for now does not support any parameters and all is set within service account credential file. provider "indykite" {} resource "time_static" "example" {} resource "indykite_event_sink" "outbound_events" { name = "outbound-events" display_name = "Outbound Events" location = "ik_project_id" providers { provider_name = "pubsub-provider" pubsub { project_id = "pub-sub-gcp-project-id" topic_name = "topic-name-in-pub-sub" credentials_json = file("path-to-file.json") } } routes { provider_id = "pubsub-provider" stop_processing = true keys_values_filter { event_type = "indykite.audit.config.*" } route_display_name = "Configuration Audit Events" route_id = "config-audit-log" } } resource "indykite_authorization_policy" "policy_drive_car" { name = "terraform-policy-drive-car" display_name = "Terraform policy drive car" description = "Policy person drive car" json = jsonencode({ meta = { policy_version = "2.0-kbac" }, subject = { type = "Person" }, actions = ["CAN_DRIVE"], resource = { type = "Car" }, condition = { cypher = "MATCH (subject)-[:DRIVES]->(resource:Car)" } }) location = "project_id" status = "active" } ``` ## Common Errors ### ALREADY_EXISTS: Only one event sink per project **Solution:** Delete the existing event sink before creating a new one, or import it into Terraform state ### INVALID_ARGUMENT: Invalid Pub/Sub configuration **Solution:** Verify GCP Service Account API key, permissions are correct --- Source: https://developer.indykite.com/terraform/terraform-7 --- # Protect an MCP server with Agent Gateway: Google Drive behind the MCP proxy > Put the IndyKite Agent Gateway (IAG) in MCP proxy mode in front of any MCP server - demonstrated end to end with a Google Drive MCP server in the iag-mcp-demo reference app. **Category:** Agent Gateway ## Summary Follow the chapters in order to understand IAG's MCP proxy mode, wrap the Google Drive MCP server into Streamable HTTP, model the wf-drive workflow in the IKG, configure the drive-mcp-iag gateway, run a Google Drive search through it, verify denials in the audit trail, remediate one live with the AuthZEN-guarded grant button, and see the same recipe extended to a graph-filtered ERP and a Salesforce CRM. This tutorial is grounded in the iag-mcp-demo reference application, running its **canbank usecase** provisioned with the instant-stack app. It puts the IndyKite Agent Gateway (IAG) in **MCP proxy mode** (`protected_agent.protocol: mcp`) in front of an MCP server - and proves the mode is downstream-agnostic by protecting a **Google Drive MCP server** that has nothing to do with IndyKite. Every request is authorized against the Identity Knowledge Graph (IKG) before it is forwarded: who the caller is, which workflow they may trigger, and through which chain of agents. And because that authorization is graph data, it changes live: a denied request is remediated with one AuthZEN-guarded click, explained with a **why?** graph, and revoked again. The closing chapter shows the same recipe carried further - a Postgres-backed ERP whose rows are pre-filtered by AuthZEN search/resource, and a CRM agent that files Salesforce cases on behalf of the delegated user. ### What you will have at the end - A clear mental model of what IAG's MCP proxy mode does and does not do (session pass-through, SSE streaming, header handling, per-request authorization). - A Google Drive MCP server running as a container: the reference stdio server wrapped into MCP Streamable HTTP. - A `Workflow` node (`external_id=wf-drive`), an `Agent` node (`indykiteagent-drive`), and the `CAN_TRIGGER` / `INVOKES` relationships in the IKG. - The `CAN_TRIGGER` KBAC policy and the ContX IQ query that resolves `(workflow, agent_list)` pairs for the gateway. - A dedicated gateway instance, `drive-mcp-iag`, proxying MCP traffic on port `8887`. - A full-text Google Drive search executed through the gateway as an authorized user - and the same call denied with `403` for everyone else, with matching audit records. - The **deny → remediate → allow** loop: a denied user's red audit card grants Drive access with one AuthZEN-guarded click (writing `CAN_TRIGGER` edges via the Capture API), a **why?** card renders the authorization path live from the graph, and a revoke resets the world. - A view of where the recipe leads: the **ERP** profile (a Postgres-backed MCP server whose rows are pre-filtered per caller by AuthZEN search/resource) and the **CRM** profile (a gateway-protected agent filing Salesforce cases on behalf of the delegated user). ### Prerequisites - An **IndyKite project** with AuthZEN/KBAC and ContX IQ enabled, provisioned with the **canbank dataset** via the instant-stack app (`DATASET=canbank`) - chapter 5 explains the parts of that dataset this tutorial relies on. - An **OAuth2-compliant IdP** with introspect, client-credentials, and token-exchange endpoints, holding the demo's agent clients - and `millicent` as a login user (chapter 7 logs in as her). - **Docker and Docker Compose**, plus a Google account for chapter 3. - The base iag-mcp-demo configured per the repo README with the **canbank usecase** active - this tutorial adds the Drive pieces on top and bootstraps the usecase env file (`.env.canbank`) in chapter 7. ### Who this tutorial is for - Developers exposing **MCP servers** to AI agents who need token introspection, authorization, and audit in front of them - without changing the MCP server itself. - Platform and security engineers who want the same enforcement point for MCP tool calls as for A2A agent calls. - AI agents consuming this doc as a runbook - every chapter uses explicit service names, ports, file paths, and commands. ### Demo topology at a glance The iag-mcp-demo is **usecase-driven**: the agents, gateways, and compose topology are domain-agnostic, and a usecase bundle (`usecases/canbank/` - domain vocabulary, per-agent skills, demo script) plus a matching provisioned IndyKite project make it "the CanBank demo". Activate it with `./switch-usecase.sh canbank`. This tutorial focuses on the services below; the Google Drive pair only starts when the `drive` compose profile is enabled. **Service** **Role** **Port** `chatbot` Web UI; users log in here, and the demo scripts reuse its session token. `3000` `mcp-iag` IAG in MCP proxy mode in front of the IndyKite MCP server. `8886` `drive-mcp-iag` IAG in MCP proxy mode in front of the Google Drive MCP server (profile `drive`). `8887` `drive-mcp` Google Drive MCP server: stdio reference server wrapped into Streamable HTTP (profile `drive`). `8000` `erp-mcp-iag` IAG in MCP proxy mode in front of the ERP MCP server (profile `erp`; chapter 8's "Beyond Drive" section). `8889` `erp-mcp` + `erp-db` Postgres-backed ERP MCP server and its database (profile `erp`). Deliberately **not** published to the host - only the gateway reaches them. internal only `analyst-iag` IAG in front of the analyst agent (used by the optional analyst path in chapter 7). `8885` `analyst` A2A data analyst agent; can use both the IndyKite and the Drive MCP backends. `6005` ## Chapter 1: What MCP proxy mode is How one configuration switch turns the Agent Gateway into an authorization-enforcing proxy for any MCP Streamable HTTP server. ## Chapter 1: What MCP proxy mode is The **IndyKite Agent Gateway (IAG)** protects exactly one downstream per instance. By default (`protected_agent.protocol: a2a`) that downstream is an A2A agent. Set `protected_agent.protocol: mcp` and the same gateway instead proxies **MCP Streamable HTTP** traffic to a downstream MCP server. Everything in front of the forwarding step - token introspection, the `CAN_TRIGGER` AuthZEN check, delegation-chain validation against the IKG, and audit - is identical in both modes. For the full product reference, see the official Agent Gateway documentation. ### What the MCP proxy does - **Accepts any MCP method on any path** - `initialize`, the `initialized` notification, `tools/list`, `tools/call`, `resources/list`, `resources/read` - and forwards it after the authorization checks pass. Session-teardown `DELETE` requests are forwarded too; whether teardown is honored is up to the downstream server. - **Passes the session through untouched** - the `Mcp-Session-Id` header is forwarded in both directions without translation. The session the downstream server mints during `initialize` is the session the caller uses on every follow-up. - **Streams SSE responses** - the downstream response body is streamed to the caller and flushed per SSE message, so long-running responses are not buffered or truncated. - **Preserves path and query** - `protected_agent.base_url` is the downstream **origin only**. The gateway resolves the incoming request path and query string on top of it, so callers use the same path the MCP server expects. Any path segment you put into `base_url` is discarded. - **Swaps the credentials** - the caller's `Authorization` header is removed and replaced with a delegation token the gateway mints at the IdP for the protected downstream. Chapter 2 covers what this means for the downstream server. ### What the MCP proxy does not do - It does **not inspect MCP payloads**. Authorization is per request and per session ("may this user reach this MCP server through this workflow?"), not per tool. Every tool the downstream exposes is reachable once a request is authorized. - It is **not a generic HTTP reverse proxy**. The downstream must speak MCP Streamable HTTP. ### Downstream-agnostic by design Nothing in MCP proxy mode is specific to IndyKite's own MCP server. The iag-mcp-demo proves this with three protected MCP downstreams: - `mcp-iag` (port `8886`) protects the **IndyKite MCP server** - the demo agents reach ContX IQ and AuthZEN tools through it. - `drive-mcp-iag` (port `8887`) protects a **Google Drive MCP server** - a third-party server that knows nothing about IndyKite. This is the pair this tutorial builds. - `erp-mcp-iag` (port `8889`, compose profile `erp`) protects a **Postgres-backed ERP MCP server** whose rows are pre-filtered per caller by the AuthZEN search/resource API before any SQL runs - the same recipe as Drive, applied to a database-backed server. ### Mental model millicent (user token) │ MCP Streamable HTTP: initialize / tools/list / tools/call ▼ drive-mcp-iag:8887 ── introspect token ──> IdP │ ── CAN_TRIGGER wf-drive ──> AuthZEN │ ── workflow chain ──> ContX IQ (IKG) │ ── AUTHORIZED / NOT_AUTHORIZED ──> audit stream │ Authorization header replaced with a delegation token, │ Mcp-Session-Id and SSE streamed through untouched ▼ drive-mcp:8000/mcp ──> Google Drive API (server's own Google credentials) ### What comes next Chapter 2 follows one MCP request through the gateway and lists the exact HTTP responses callers can get back. ## Chapter 2: How the gateway handles an MCP request The runtime path of one MCP call through IAG, the credential swap, and every HTTP response a caller can receive. ## Chapter 2: How the gateway handles an MCP request Take one call - `tools/call` with the Drive server's `search` tool - sent to `http://localhost:8887/mcp` with a user's Bearer token. The gateway runs the same sequence for every request in the session: - **Extract the Bearer token.** No `Authorization: Bearer` header means an immediate `401`. - **Introspect the token at the IdP.** The token must be active and carry a subject. Delegated tokens carry an `act` claim naming the chain of actors the request has passed through; a request straight from a user has no chain yet. - **Resolve the allowed workflows from the IKG.** Via the configured ContX IQ query, the gateway asks: for the agent I protect (`indykiteagent-drive`), which workflows invoke it, and through which agent chain? The answer comes from the `Workflow`/`Agent` nodes and `INVOKES` relationships you model in chapter 5. - **Check the subject with AuthZEN.** Can this subject `CAN_TRIGGER` one of those workflows? This evaluates the KBAC policy you create in chapter 5 against the graph. - **Validate the delegation chain.** The actors in the token's `act` chain must match one of the workflow's modeled agent chains. - **Audit the decision.** An `AUTHORIZED` or `NOT_AUTHORIZED` record is emitted on the configured audit stream (chapter 8). - **Mint the downstream credential and forward.** The gateway obtains its own token at the IdP (client credentials plus token exchange, using the `client_id`/`client_secret` configured for this instance), **replaces** the caller's `Authorization` header with it, and forwards the request - same path, same query, same body, `Mcp-Session-Id` untouched. The downstream's status code, headers, and (streamed) body go back to the caller as-is. ### The credential swap, and what it means for your MCP server Because the gateway always replaces the `Authorization` header, the protected MCP server never sees the caller's token - it sees a delegation token identifying the gateway's IdP client. Two consequences: - An MCP server that requires the **caller's own upstream credential** (for example a hosted MCP endpoint that expects the caller's Google OAuth access token) will not work behind the gateway. - The downstream must hold **its own credentials** for whatever backend it talks to. The Google Drive server in this tutorial holds its own Google OAuth material (chapter 3) and simply ignores the incoming Bearer token. ### Responses a caller can receive **Status** **Body / meaning** `200` / `202` Authorized and forwarded; this is the downstream MCP server's own response (JSON or SSE). `400` Bad request - the request could not be processed because of its content. `401` `{"message": "Missing bearer token"}` when no token is sent. Other `401` messages indicate an inactive token or one missing required claims after introspection. `403` `{"message": "Authorization check failed"}` - the caller is authenticated but the subject cannot trigger the workflow, or the delegation chain does not match any modeled chain. `500` `{"message": "Internal Server Error"}`. `502` The downstream MCP server could not be reached or the forward failed. The `401`-with-no-token response doubles as a liveness probe: an unauthenticated `POST` to a gateway port answering `{"message":"Missing bearer token"}` proves the instance is up and enforcing. Chapter 7 uses exactly this check. ### What comes next Chapter 3 sets up the Google side: a Google Cloud project, the Drive API, and the OAuth material the Drive MCP server needs. ## Chapter 3: Set up Google Drive access Create the Google Cloud OAuth client and mint the credentials the Drive MCP server uses to search Google Drive. ## Chapter 3: Set up Google Drive access The Drive MCP server talks to the Google Drive API with **its own** Google OAuth credentials (chapter 2 explains why the caller's token never reaches it). This chapter produces the two credential files the server needs. Work from the demo directory: `cd a2a/iag-mcp-demo` ### 1. Create a Google Cloud project and enable the Drive API - Go to console.cloud.google.com. A personal Google account is easiest: it can create projects freely, and the demo then searches that account's Drive. - Create a project (for example `iag-drive-demo`) and switch to it. - Search for **Google Drive API** and click **Enable**. ### 2. Configure the OAuth consent screen Under **APIs & Services → OAuth consent screen**: choose type *External*, fill the required fields, and under **Test users** add the Google account you will authorize with. While the app is in *Testing* status only test users can authenticate - anyone else gets "Access blocked: app has not completed verification". ### 3. Create the OAuth client and download the keys - **APIs & Services → Credentials → Create credentials → OAuth client ID** → application type **Desktop app** → Create → **Download JSON**. - Save the download as `drive_mcp/.gdrive/gcp-oauth.keys.json` inside the demo directory. This path is git-ignored. ### 4. Authorize once (browser flow) Run the one-time auth bootstrap on the host. It opens a browser and writes the refresh-token credentials file next to the keys: cd drive_mcp GDRIVE_OAUTH_PATH=$PWD/.gdrive/gcp-oauth.keys.json \ GDRIVE_CREDENTIALS_PATH=$PWD/.gdrive/.gdrive-server-credentials.json \ npx -y @modelcontextprotocol/server-gdrive auth cd .. In the browser: pick the account (the project owner or a test user) → on the "Google hasn't verified this app" warning click **Advanced** → **Go to (unsafe)** (it is your own app) → **Allow** read access to Drive. The granted scope is read-only (`https://www.googleapis.com/auth/drive.readonly`). ### 5. Verify the credential files ls -A drive_mcp/.gdrive/ # expect both files (next to the repo's committed .gitkeep): # gcp-oauth.keys.json # .gdrive-server-credentials.json Both files must come from the **same** OAuth client. If you ever replace `gcp-oauth.keys.json`, re-run step 4 - mismatched files fail later with `invalid_request` on the first Drive call. ### 6. Seed some content Put a few files in that Google account's Drive so searches return something. The Drive server's `search` tool is **full-text over file contents**, so a reliable trick is to create a Google Doc containing a unique word (for example `canbank-test-fixture`) - searching for that word then always returns exactly that document. ### What comes next Chapter 4 packages the Drive MCP server as a container the gateway can protect. ## Chapter 4: The Google Drive MCP server container Wrap the reference stdio Drive MCP server into MCP Streamable HTTP so the gateway can proxy it. ## Chapter 4: The Google Drive MCP server container The gateway proxies **MCP Streamable HTTP**, but the reference Google Drive MCP server speaks **stdio**. The demo bridges the two and exposes it as a stateful Streamable HTTP endpoint. This is a general recipe: any stdio MCP server can be put behind IAG the same way. ### The image The `drive_mcp/Dockerfile` in the demo installs the gateway and the vendored Drive server - the reference `server-gdrive` extended with two demo-friendly touches: `search` results include the file ID, and **PDF files are text-extracted on read** instead of returned as opaque base64 (the reference server only exports Google-native docs as text). - MCP endpoint: `http://drive-mcp:8000/mcp`. This is the path callers will also use on the gateway, since the gateway forwards the incoming path onto the downstream origin. - `--stateful`: the gateway mints an `Mcp-Session-Id` per `initialize`; the gateway passes it through both ways. - Health check: `GET /healthz`. ### The compose service drive-mcp: image: drive-mcp:latest profiles: ["drive"] ports: - "8000:8000" networks: - drive-mcp-iag-network environment: # Paths to the mounted Google OAuth material (values are paths, not secrets). GDRIVE_OAUTH_PATH: /gdrive/gcp-oauth.keys.json GDRIVE_CREDENTIALS_PATH: /gdrive/.gdrive-server-credentials.json volumes: - ./drive_mcp/.gdrive:/gdrive:ro The credential files from chapter 3 are mounted read-only. The server refreshes its Google access token from them automatically; the Bearer token the gateway injects on forwarded requests is simply ignored - exactly the arrangement chapter 2 requires. ### What the server exposes **Capability** **Behavior** Tool `search` Full-text search over file *contents* in the authorized account's Drive; returns up to 10 matches, one ` ()` line each. `resources/list` Lists Drive files as resources with `gdrive:///` URIs. `resources/read` Reads one file by URI. Google-native files (Docs, Sheets, Slides) are exported as text, and PDFs are text-extracted by the vendored server (an encrypted or scanned-image PDF falls back to base64). Other binaries (`.doc`, video, …) come back base64-encoded, so point read prompts at Google-native or PDF copies. ### Build it `make new-drive-mcp # builds the drive-mcp image from ./drive_mcp` ### What comes next The server is ready but nothing authorizes access to it yet. Chapter 5 models who may reach it, and through which chain, in the IKG. ## Chapter 5: Model the wf-drive workflow in the IKG The Workflow and Agent nodes, CAN_TRIGGER grant, and INVOKES chain that authorize Google Drive access - plus one workflow per call shape. ## Chapter 5: Model the wf-drive workflow in the IKG The gateway authorizes an MCP request only if (a) the subject can `CAN_TRIGGER` a workflow that invokes the protected agent and (b) the request's delegation chain matches that workflow's modeled agent chain. Both facts live in the Identity Knowledge Graph (IKG). This chapter adds them for the Drive downstream. ### 1. Register the IdP client Create a machine-to-machine client `indykiteagent-drive` in your IdP (client credentials and token exchange flows; no redirect URI). **The client ID must exactly match the Agent node's `external_id`** - the gateway matches delegation-chain actors against agent IDs from the graph. ### 2. Create the nodes Via the Capture API (`POST /capture/v1/nodes`) - one `Workflow` and one `Agent`: { "nodes": [ { "external_id": "wf-drive", "type": "Workflow", "is_identity": false, "properties": [] }, { "external_id": "indykiteagent-drive", "type": "Agent", "is_identity": false, "properties": [] } ] } ### 3. Create the relationships Via `POST /capture/v1/relationships`. Two edges matter: the **grant** (who may trigger the workflow) and the **chain** (which agent the workflow invokes). Every `INVOKES` edge in a chain must carry a `workflow_name` property equal to the workflow's `external_id` - the workflow-resolution query filters on it at every hop and silently drops chains without it. { "relationships": [ { "source": { "type": "User", "external_id": "millicent" }, "target": { "type": "Workflow", "external_id": "wf-drive" }, "properties": [], "type": "CAN_TRIGGER" }, { "source": { "type": "Workflow", "external_id": "wf-drive" }, "target": { "type": "Agent", "external_id": "indykiteagent-drive" }, "properties": [ { "type": "workflow_name", "value": "wf-drive" } ], "type": "INVOKES" } ] } In the demo dataset only `millicent` gets this grant. Every other user is denied at the gateway - that asymmetry is the demo. ### 4. Create the KBAC policy The gateway's AuthZEN check needs one policy answering "can this subject trigger this workflow?". It is generic over all workflows - if you already provisioned it for other gateway instances, there is nothing to add; `wf-drive` is covered the moment it exists in the graph. { "meta": { "policy_version": "2.0-kbac" }, "subject": { "type": "User" }, "actions": [ "CAN_TRIGGER" ], "resource": { "type": "Workflow" }, "condition": { "cypher": "MATCH (subject)-[:CAN_TRIGGER]->(resource:Workflow)" } } The subject type must be listed in the gateway's `authzen.subject_types` setting (the demo configures `User`). If you use more subject types, create one policy per type. The direct-edge condition above is all `wf-drive` needs. The canbank dataset provisions a department-aware variant, `user-can-trigger-workflow` - `MATCH (subject:User)-[:WORKS_IN|CAN_TRIGGER*..3]->(resource:Workflow)` - so users also inherit workflow access through their department (a `User -WORKS_IN-> Department -CAN_TRIGGER-> Workflow` path). That is how the support and trading staff reach `wf1` in chapter 7's persona prompts. Workflow triggering is not the dataset's only policy. It also provisions `user-can-retrieve-quote` (`2.0-kbac`): subject `User`, action `CAN_RETRIEVE`, resource `Quote` (id `stock_quote`), condition `MATCH (subject:User)-[:WORKS_IN]->(department:Department)-[:CAN_RETRIEVE]->(resource:Quote)`. Only the `trading` department holds the `CAN_RETRIEVE` edge, and `millicent` works in both `support` and `trading` - this is what powers the stock-quote and AuthZEN prompts in chapter 7. ### 5. Create the workflow-resolution ContX IQ query The gateway's second question - "which workflows invoke the agent I protect, through which chains?" - is answered by a ContX IQ query (the demo names it `get-agent-workflows`). Given `$agent_id`, it returns one `(workflow, agent_list)` pair per allowed chain, keeping only chains whose every hop carries the matching `workflow_name`: { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "_Application" }, "condition": { "cypher": "MATCH (subject:_Application) MATCH (wf:Workflow)-[rels:INVOKES*]->(a:Agent {external_id: $agent_id}) WHERE ALL(r IN rels WHERE r.workflow_name = wf.external_id AND endNode(r):Agent) WITH subject, wf.external_id AS workflow, [r IN rels | endNode(r).external_id] AS agent_list", "filter": [] }, "allowed_reads": { "nodes": [], "relationships": [], "aggregate_values": [ "workflow", "agent_list" ] } } This query is also shared by every gateway instance - record its name or GID once; it goes into the gateway configuration as the ContX IQ query ID (chapter 6). With the graph from this chapter fully modeled, running it for `agent_id = indykiteagent-drive` returns one row per call shape (the two extra shapes are modeled in the next section): { "data": [ { "aggregate_values": { "workflow": "wf-drive", "agent_list": ["indykiteagent-drive"] } }, { "aggregate_values": { "workflow": "wf-drive-analyst", "agent_list": ["indykiteagent-4", "indykiteagent-drive"] } }, { "aggregate_values": { "workflow": "wf-drive-console", "agent_list": ["indykiteagent", "indykiteagent-4", "indykiteagent-drive"] } } ] } ### One workflow per call shape The gateway resolves **one agent chain per workflow**. If the same protected agent should be reachable through several different chains - directly, via another agent, via a console orchestrator - model each shape as its own workflow. The demo wires four: **Workflow** **Chain** **Used by** `wf-drive` `indykiteagent-drive` Direct MCP calls to the Drive gateway (chapter 7). `wf-drive-analyst` `indykiteagent-4 → indykiteagent-drive` A prompt to the analyst agent, which calls Drive as an MCP backend. `wf-drive-console` `indykiteagent → indykiteagent-4 → indykiteagent-drive` The chatbot console: orchestrator → analyst → Drive. `wf3-console` `indykiteagent → indykiteagent-4 → indykiteagent-mcp` The console-routed analyst reaching the IndyKite MCP server. When two workflows share an agent-to-agent hop (for example `indykiteagent-4 → indykiteagent-drive` in both the analyst and console shapes), create **parallel `INVOKES` edges** - one per workflow - and add a `discriminating_property` property with value `workflow_name` to each, so the edges stay distinct per workflow: { "source": { "type": "Agent", "external_id": "indykiteagent-4" }, "target": { "type": "Agent", "external_id": "indykiteagent-drive" }, "properties": [ { "type": "workflow_name", "value": "wf-drive-analyst" }, { "type": "discriminating_property", "value": "workflow_name" } ], "type": "INVOKES" } Grant `CAN_TRIGGER` per shape as well - in the demo, `millicent` holds it on all three `wf-drive*` workflows. ### Provisioning shortcut The instant-stack companion app provisions everything in this chapter - and the rest of the project - from the canbank dataset (`DATASET=canbank`, the default). Its **Provision Everything** run works through the manifest in dependency order: project → application → App Agent (with credentials) → Token Introspect → MCP server configuration, then the graph capture (nodes and relationships, including all the `wf-drive*` workflows and `indykiteagent-drive` with their `workflow_name` / `discriminating_property` edges), the KBAC policies, the external data resolvers, and every ContX IQ policy + knowledge-query pair (`get-agent-workflows` included). Each created ID is recorded in the app's `.env` - that output is where chapter 7's env values come from. The run is safe to repeat: existing elements are skipped and graph ingestion is an upsert. The demo repo also carries the same data request-by-request as a Bruno collection (`bruno/iag-demo`, workflow graph under `ingest/agent-workflow`) - useful as a manual alternative or for debugging a single element. ### What comes next Chapter 6 configures the gateway instance that enforces all of this: `drive-mcp-iag`. ## Chapter 6: Configure the drive-mcp-iag gateway The per-instance settings that switch IAG into MCP proxy mode and point it at the Drive server. ## Chapter 6: Configure the drive-mcp-iag gateway The gateway takes its configuration from a YAML file passed with `--config`, from environment variables, or both. Every config path maps to an environment variable with the fixed `JARVIS_` prefix: uppercase the path and replace dots with underscores, so `protected_agent.protocol` becomes `JARVIS_PROTECTED_AGENT_PROTOCOL`. The demo uses a shared base service (`iag-base-docker.yaml`) holding the common IdP, AuthZEN, ContX IQ, and audit settings, and each instance overrides only its per-instance fields. Use a gateway image with MCP proxy support - the demo pins `indykite/agent-gateway:2.52.1`. ### The compose service drive-mcp-iag: extends: service: iag-base file: iag-base-docker.yaml profiles: ["drive"] ports: - "8887:8887" networks: - drive-mcp-iag-network environment: JARVIS_SERVICE_NAME: drive-mcp-iag JARVIS_SERVICE_PORT: 8887 # Switch from the default "a2a" proxy into MCP proxy mode. JARVIS_PROTECTED_AGENT_PROTOCOL: mcp # Origin only - the gateway forwards the incoming path (/mcp) on top of it. JARVIS_PROTECTED_AGENT_BASE_URL: http://drive-mcp:8000 JARVIS_PROTECTED_AGENT_AUTHENTICATION_CLIENT_ID: ${DRIVE_MCP_IDP_CLIENT_ID:-indykiteagent-drive} JARVIS_PROTECTED_AGENT_AUTHENTICATION_CLIENT_SECRET: ${DRIVE_MCP_IDP_CLIENT_SECRET} # Empty = allow any workflow resolved from the graph. Required here because # three call shapes (wf-drive, wf-drive-analyst, wf-drive-console) converge # on this gateway; set a single workflow id to pin one shape only. JARVIS_CONTX_IQ_ALLOWED_WORKFLOW_ID: ${DRIVE_WORKFLOW_ID:-} # Audit delivery is configured per instance via a mounted config file. volumes: - ./audit-config.yaml:/app/.configs/audit-config.yaml command: ["--config=/app/.configs/audit-config.yaml"] ### The per-instance fields **Setting** **Value for this instance** `service.name` `drive-mcp-iag` - also stamped on every audit record. `service.port` `8887`. `protected_agent.protocol` `mcp`. Anything other than `a2a` or `mcp` fails startup with *invalid protected_agent protocol*. `protected_agent.base_url` `http://drive-mcp:8000` - origin only, no path. `protected_agent.authentication.client_id` / `client_secret` The `indykiteagent-drive` IdP client from chapter 5. `contx_iq.allowed_workflow_id` Empty, so all workflows resolved from the graph authorize - this gateway serves three call shapes. The IdP, AuthZEN (action `CAN_TRIGGER`, subject type `User`), and ContX IQ settings (the `get-agent-workflows` query ID and App Agent credentials token) are inherited from the shared base and identical to the other gateway instances. **Audit delivery** comes from the mounted `audit-config.yaml` shown above - the same file every gateway in the demo mounts; without it the instance runs with auditing disabled, and chapter 8 has nothing to read. **Workflow-set caching:** each gateway caches the workflow set it resolves per subject, and the demo keeps the image defaults (5-minute TTL). Leave those defaults in place - the demo explicitly warns against faster settings. The practical consequence: a `CAN_TRIGGER` edge added or removed in the graph (chapter 8's grant/revoke flow) takes effect on a gateway only when its cache entry expires, typically up to ~5 minutes - or immediately after restarting the affected gateways (`docker compose restart orchestrator-iag analyst-iag drive-mcp-iag`). The platform's own AuthZEN answers change at once; only the gateway cache lags. ### .env entries These go into the usecase's env file, `.env.canbank` - the demo keeps one complete env file per usecase and symlinks `.env` to the active one (chapter 7). # Include the two drive services in a plain `docker compose up` # (equivalent to `docker compose --profile drive up`); leave empty for the base demo. COMPOSE_PROFILES=drive # IdP client for drive-mcp-iag's client-credentials / token-exchange flow. DRIVE_MCP_IDP_CLIENT_ID=indykiteagent-drive DRIVE_MCP_IDP_CLIENT_SECRET= # Empty = all wf-drive* call shapes authorize; set to wf-drive to pin direct calls only. DRIVE_WORKFLOW_ID= # Optional: give the analyst agent both MCP backends (alias=url pairs). # Tool names get prefixed per backend: indykite_ciq_execute, drive_search, ... ANALYST_MCP_SERVER_URLS=indykite=http://mcp-iag:8886/mcp/v1/,drive=http://drive-mcp-iag:8887/mcp # Optional: enable the chatbot's grant/revoke buttons on DENY cards (chapter 8). # Per gateway, the workflows one click grants (colon-separated); empty = buttons off. # Only the chatbot reads this - the gateways stay unpinned. GRANT_WORKFLOW_MAP=analyst-iag=wf-drive:wf-drive-analyst:wf-drive-console,drive-mcp-iag=wf-drive:wf-drive-analyst:wf-drive-console # Optional: enable the "why?" buttons on audit cards (chapter 8) - the GIDs of the # two explain queries provisioned with the canbank dataset (instant-stack slots 11/12). EXPLAIN_STAFF_QUERY_ID= EXPLAIN_DIRECT_QUERY_ID= ### What comes next Chapter 7 starts the stack and runs a Google Drive search through the gateway. ## Chapter 7: Run a Google Drive search through the gateway Start the stack, establish an MCP session against drive-mcp-iag, and execute the full-text search - by script, by hand, and by prompt. ## Chapter 7: Run a Google Drive search through the gateway ### 1. Configure the usecase env file The demo keeps one **complete env file per usecase** (`.env.canbank`, gitignored) and `.env` is a symlink to the active one - so a switch always swaps the platform bindings, the domain vocabulary, and the agent skills together. First run only: bootstrap `.env.canbank` and fill in the base values per the repo README - the IndyKite base URL, the `get-agent-workflows` query ID, the App Agent credentials token, the IdP client secrets, `USECASE=canbank`, and an LLM key for the agents - then add the Drive entries from chapter 6 (including `COMPOSE_PROFILES=drive`). Take the project-specific values from the instant-stack provisioning output (chapter 5), which records every created ID. cd a2a/iag-mcp-demo cp .example.env .env.canbank # edit .env.canbank: base demo values per the repo README + the chapter 6 drive entries ./switch-usecase.sh canbank # links .env -> .env.canbank (and recreates changed containers) `USECASE=canbank` selects the usecase bundle `usecases/canbank/`: its `usecase_name.env` feeds every agent the CanBank vocabulary (org name, knowledge-query names such as `get-stock-quote` and `get-hq-weather`, the weather HQ keywords), and its `skills/` folders are mounted read-only into the orchestrator, retriever, and analyst - including the dataset-specific `canbank-authz` skill with the exact KBAC vocabulary. Two optional `.env` entries tune the agents. `MCP_SESSION_TTL` (default `300`) is how long, in seconds, an agent reuses each user's MCP session against the gateway before rebuilding it - keep it below the access-token lifetime, or set `0` for a fresh session per request. `LIB_LOG_LEVEL` (default `INFO`) sets the log level of the agents' third-party libraries independently of `LOG_LEVEL` - keep it at `INFO` for readable agent logs, or set `DEBUG` to see the SDK events as one-line breadcrumbs. ### 2. Build and start the stack make # builds the chatbot and agent images make new-drive-mcp # builds the drive-mcp image (chapter 4) docker compose up -d # COMPOSE_PROFILES=drive in .env.canbank pulls in the two drive services docker compose ps With the `drive` profile enabled expect **12 containers** - the 10 base services plus the `drive-mcp` / `drive-mcp-iag` pair; fewer means the profile didn't activate. (The demo's other optional profiles, `crm` and `erp`, add further containers - chapter 8's "Beyond Drive" section covers what they do.) Confirm the drive pair and prove the gateway is enforcing: docker compose logs drive-mcp | tail # the wrapped server is listening on :8000, endpoint /mcp docker compose logs drive-mcp-iag | tail # the gateway is listening on :8887 curl -s -X POST http://localhost:8887/mcp -H "Content-Type: application/json" -d '{}' # {"message":"Missing bearer token"} <- alive and enforcing (chapter 2) ### 3. Get an authorized user token Open `http://localhost:3000` (use `localhost`, not `127.0.0.1`) in a fresh incognito window and log in as **millicent** - the one user granted `CAN_TRIGGER` on `wf-drive` in chapter 5. She must exist as a login user in your IdP; if she does not, either create her there or grant your own login user the `CAN_TRIGGER` edges instead. The demo's test scripts extract the logged-in user's access token from the chatbot session automatically; for the manual calls below, export it as `TOKEN`. Tokens are short-lived, so run the calls soon after logging in. ### 4. The script version ./test-drive.sh # initialize → tools/list → resources/list → search, as millicent DRIVE_QUERY=budget ./test-drive.sh # different search term ./test-drive.sh # as a different logged-in user ### 5. The manual version An MCP session against the gateway, step by step. All requests go to the **gateway** (`:8887/mcp`), never to the Drive server directly: DRIVE="http://localhost:8887/mcp" H=(-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream") # 1. initialize - capture the MCP session id from the response headers SID=$(curl -s -D - -o /dev/null "${H[@]}" -X POST $DRIVE \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"demo","version":"1.0"}}}' \ | grep -i mcp-session-id | tr -d '\r' | awk '{print $2}') echo "session: $SID" # a UUID; empty means check the token or the gateway logs # 2. initialized notification (expect HTTP 202) curl -s -o /dev/null -w "initialized -> HTTP %{http_code}\n" "${H[@]}" \ -H "Mcp-Session-Id: $SID" -X POST $DRIVE \ -d '{"jsonrpc":"2.0","method":"initialized","params":{}}' # 3. list the Drive server's tools (expect: search) curl -s "${H[@]}" -H "Mcp-Session-Id: $SID" -X POST $DRIVE \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' # 4. the search - full-text over the authorized account's Drive curl -s -m 45 "${H[@]}" -H "Mcp-Session-Id: $SID" -X POST $DRIVE \ -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"search","arguments":{"query":"canbank"}}}' Expected result of the search - lines of ` ()`; your files will differ, and matches are files whose *content* mentions the query; for example: Found 3 files: 1AbC...xYz Financial report (application/vnd.google-apps.document) 1DeF...uVw Retail campaign (application/vnd.google-apps.document) 1GhI...rSt video1.mp4 (video/mp4) Cross-check: open drive.google.com as the authorized Google account and type the same query into the search bar - same files. Between steps, note what just happened: the gateway introspected millicent's token, confirmed `CAN_TRIGGER wf-drive` against the graph, audited the decision, swapped in its delegation token, and streamed the Drive server's response back - while the `Mcp-Session-Id` minted downstream flowed through untouched. ### 6. The prompt versions (optional) The same authorization guards the longer chains modeled in chapter 5: - **Via the analyst** (`wf-drive-analyst`): with `ANALYST_MCP_SERVER_URLS` set (chapter 6), send the analyst gateway (`:8885`) an A2A `message/send` with a prompt like *"Search Google Drive for canbank"* - the analyst then calls Drive as one of its MCP backends. Cross-backend prompts work too: *"Search Google Drive for canbank, then check which internal policy documents mention the same topics."* (The repo's `./demo-analyst-drive.sh` is a related smoke test: it sends the analyst a hello message and then runs the *direct* Drive search from step 5.) - **Via the chatbot console** (`wf-drive-console`): as millicent, type *"Search Google Drive for canbank and list the matching files"* at `http://localhost:3000`. Mentioning **"Google Drive"** (or "Drive") in the prompt is what routes it to the orchestrator's `query_drive` tool instead of the retriever - a prompt like *"search my files for canbank"* silently goes to the retriever. Every hop - orchestrator, analyst, Drive - passes its own gateway and leaves its own audit record. Prompt tips: Drive search is full-text, so to target a specific file prefer listing then reading (*"Read the file 'X' from Google Drive and summarize it"*), and point read prompts at Google-native documents or PDFs - the only formats that come back as readable text (chapter 4). For workflow prompts, **name the agent** (*"Use the retriever to …"*, *"Ask the retriever for …"*) so the orchestrator actually delegates instead of answering itself. Stock-quote prompts fetch live market data; an occasional `429 Too Many Requests` is upstream rate-limiting, not an authorization failure - retry after a few minutes. ### 7. Prompts by persona The graph gives every demo user different access, so the same prompt can succeed for one login and be denied - or return nothing - for another. That asymmetry *is* the demo. Switch personas with a fresh incognito window per user. The repo's scripted tour of these prompts lives in `usecases/canbank/DEMO_SCRIPT.md`. Chatbot console prompts: **Persona (access)** **Prompts that work** **What is denied** `millicent` - support + trading depts; `wf1`, `wf3`, all `wf-drive*` Everything leslie can, plus the star turn: *"Search Google Drive for canbank and list the matching files."*, *"List the files in my Google Drive."*, *"Read the file 'X' from Google Drive and summarize it."* Trading membership adds stock and AuthZEN prompts: *"What is the price of META?"*, *"Am I allowed to retrieve a stock quote? Check with authzen."* (decision `true`). Asking authzen about a *different* user (e.g. subject `roy`) returns `false` by design - see the note below the table. `leslie` - support dept → `wf1` *"Use the retriever to find internal policy documents about refunds."*, *"Ask the retriever which past decisions incorporated the refund_policy document."*, *"What's the weather in London?"* Drive and analyst calls → `403`; stock prompts return nothing, and *"Am I allowed to retrieve a stock quote?"* answers `false` - support holds no `CAN_RETRIEVE` edge. `roy` - trading dept → `wf1` *"Use the retriever to get the NVDA stock price."*, *"Ask the retriever how many shares of NVDA the customer rebecca can purchase."*, *"Am I allowed to retrieve a stock quote? Check with authzen."* (decision `true`). Drive and analyst calls → `403` - which makes roy the demo's grant-flow persona: chapter 8's deny → remediate → allow beat grants him Drive access with one click and revokes it again. `rebecca` - customer, direct `wf1` grant *"Who am I?"*, *"Show my profile"*, prompts about her own accounts and documents. Department-scoped queries (internal docs, stock) return nothing - she has no department; Drive → `403`. `carol` - `wf1` only Normal `wf1` prompts (as leslie). The designated deny persona: millicent's exact analyst and Drive requests with carol's token → `403`. The "same request, different user, different outcome" money shot. `jane` - `wf2` only Weather prompts: *"What's the weather in London?"*, *"What's the weather at CanBank HQ?"* The HQ variant goes through the `get-hq-weather` knowledge query and its weather resolvers only when `CIQ_QUERY_HQ_WEATHER` and that setup exist; otherwise weather prompts fall back to direct Open-Meteo. All `wf1` flows → `403` - she cannot trigger the orchestrator workflow. `joe` - plain `wf1` Baseline `wf1` prompts; handy as the default subject for a direct AuthZEN `CAN_TRIGGER` evaluation. Analyst and Drive → `403`. Note the three kinds of "no" in the table: a `403` is the **gateway** denying the workflow; an empty result is **data-level** scoping inside an authorized workflow (the retriever's queries respect department reach); an AuthZEN decision of `false` is a **policy** answer delivered inside an authorized workflow - leslie asking about the stock quote gets a clean `false`, not an error. One more `false` is by design rather than policy: evaluations through the chatbot carry the logged-in user's token, and the platform binds them to that token's subject - asking about a *different* user always returns `false`, regardless of that user's real permissions. Evaluate other subjects from a service context instead (the repo's Bruno `authzen` folder, `X-IK-ClientKey` app token). When writing your own AuthZEN prompts or agents, use the policy vocabulary exactly: types, actions, and ids are case-sensitive graph terms (`User`, `CAN_RETRIEVE`, `Quote`, `stock_quote`). A guessed value such as `user`, `view`, or `stock` evaluates to a `false` indistinguishable from a real denial, and the MCP server's `list_resources` returns only knowledge queries, not policy vocabulary - so the demo agents carry the exact triples as agent skills; give your own agents the vocabulary the same explicit way. Which personas can actually *log in* depends on your IdP - the graph side comes from the chapter 5 ingest; if a persona cannot log in, create them in the IdP or grant your login user the equivalent edges. ### What comes next A successful search is only half the story. Chapter 8 proves the denials and reads the audit trail. ## Chapter 8: Verify denials and read the audit trail Force NOT_AUTHORIZED outcomes on purpose, read the audit records, and fix the common failure modes. ## Chapter 8: Verify denials and read the audit trail Confidence in an enforcement point comes from watching it say no. Tail the gateway while you test: `docker compose logs -f drive-mcp-iag` ### The money shot: same request, different user Log in as any user other than millicent (fresh incognito window) and repeat chapter 7's calls - `./test-drive.sh leslie`, or the manual sequence with that user's token. Every request now returns: HTTP 403 {"message": "Authorization check failed"} Only millicent holds `CAN_TRIGGER` on `wf-drive` in the graph. Nothing else changed: same gateway, same MCP server, same request body - different subject, different decision. ### Other denial paths worth forcing once - **Skip the chain** - call the Drive gateway with a token delegated for a different workflow: the actors in the token's `act` chain match no `wf-drive*` chain → `403`. - **Break the model** - remove the `workflow_name` property from the `INVOKES` edge: the workflow-resolution query stops returning the chain → `403`. - **Revoke the grant** - delete the `CAN_TRIGGER` edge: AuthZEN answers no → `403`. ### Deny → remediate → allow: the grant button Authorization is data, and the demo makes that literal. With `GRANT_WORKFLOW_MAP` set (chapter 6), every red **NOT AUTHORIZED** card in the chatbot's audit terminal carries a **grant access** button. The scripted beat, as roy: - roy: *"List the files in my Google Drive."* → red NOT AUTHORIZED card from `drive-mcp-iag`, with **why?** and **grant access** buttons. - roy clicks **grant access** on his own card → `403`: the grant is guarded by a live AuthZEN self-check - it only proceeds when the *logged-in* user can `CAN_TRIGGER` every workflow in the bundle themselves. roy cannot self-serve. - millicent clicks **grant access** on the same card in her console (she holds the drive workflows) → a Capture API upsert writes `roy -CAN_TRIGGER-> wf-drive*` edges, and the why? graph shows the new path immediately. - roy repeats the prompt → **green**, the real Drive listing - once the gateway's workflow-set cache clears (up to ~5 minutes on the image-default TTL, or restart the gateways: `docker compose restart orchestrator-iag analyst-iag drive-mcp-iag`). - millicent clicks **revoke access** (a Capture delete of the same edges) → after the same cache window, roy is red again. The beat is repeatable. Nothing but graph edges changed between the red and green runs - same gateways, same MCP server, same prompt. ### Explain the decision: the why? cards With `EXPLAIN_STAFF_QUERY_ID` and `EXPLAIN_DIRECT_QUERY_ID` set (chapter 6), every AUTHORIZED / NOT AUTHORIZED card also carries a **why?** button that renders the authorization path behind the decision, live from the graph: the staff leg (`Subject -WORKS_IN-> Department -CAN_TRIGGER-> Workflow`) and the direct leg (`Subject -CAN_TRIGGER-> Workflow`). A denial renders as the two nodes with *no connecting path* - the graph shows why not. The two explain queries ship with the canbank dataset (instant-stack slots 11/12); because they run live, a grant or revoke changes the picture on the next click. ### Reading the audit records Every decision emits one record on the audit stream configured in the mounted audit config (chapter 6): delivered either to a **webhook** (`audit.delivery: webhook` plus the target URL and auth) or to a **file** (`audit.delivery: file` with a storage path, `csv`/`json`/`txt` format, and size- or time-based rotation). The demo posts records to a webhook on the chatbot, which displays them live. The record fields: **Field** **Meaning** `decision` `AUTHORIZED` or `NOT_AUTHORIZED`. `reason` Human-readable explanation, e.g. `subject can trigger workflows wf-drive and the actors in the delegation chain`, or on denial `subject is not authorized to trigger any of the workflows ...` / `no workflow matches the actors chain`. `subject` The end user the request acts for (e.g. `millicent`). `actor` / `actorsChain` The immediate caller and the full ordered delegation chain from the token's `act` claim. `action` The action evaluated for the decision. `service` The gateway instance that decided (`drive-mcp-iag`) - how you tell the hops apart in a multi-gateway chain. `timestamp` / `traceID` RFC 3339 UTC time and the trace ID correlating gateway, downstream, and client logs. On a console-path request (chapter 7, `wf-drive-console`) you should see `AUTHORIZED` records from every gateway the request traversed - orchestrator, analyst, and Drive - each with a growing `actorsChain`. ### Troubleshooting **Symptom** **Cause / fix** `401 Missing bearer token` on your own calls Token missing or expired - tokens are short-lived; log in again and re-export `TOKEN`. `403` for a user you expected to pass The subject lacks `CAN_TRIGGER` on the workflow, or the graph model is missing an edge or its `workflow_name` - re-run the ingest from chapter 5. AuthZEN decision `false` when you expected `true` Check the vocabulary casing (`User`, `CAN_RETRIEVE`, `Quote` - a mistyped type or action evaluates `false` with no error) and the subject: evaluations carrying a user token are bound to that token's subject, so another subject always evaluates `false` - evaluate other users from a service context (`X-IK-ClientKey`). Empty `Mcp-Session-Id` after `initialize` Check `docker compose logs drive-mcp-iag drive-mcp` - usually the graph ingest or the IdP client for `indykiteagent-drive` is missing. `404` on MCP methods The gateway image predates MCP proxy support - use the version pinned in chapter 6 or newer. A grant or revoke seems to have no effect Each gateway caches the workflow set it resolves per subject (~5-minute image-default TTL, chapter 6) - wait out the window or restart the affected gateways to clear the cache. The graph and AuthZEN answers change immediately; only the gateway cache lags. Search fails with `invalid_request` or credential errors The two Google files in `drive_mcp/.gdrive/` come from different OAuth clients or are missing - redo chapter 3 step 4. File reads answer "binary document" Only Google-native files and PDFs come back as text (chapter 4); an encrypted or scanned-image PDF also falls back to base64 - convert the file to a Google Doc. ### Beyond Drive: two more protected downstreams The demo extends the same enforcement pattern in two optional directions. Neither is required for this tutorial, but both show where the recipe leads. Graph-filtered ERP rows (compose profile `erp`) A second non-IndyKite MCP server behind its own gateway - this time a plain **Postgres "ERP"** (one invoices table) whose rows are pre-filtered per caller by the knowledge graph before any SQL runs. Where Drive showcased MCP proxying, ERP showcases the **AuthZEN search/resource API** (enumeration) alongside the evaluate API (yes/no) used everywhere else: console → orchestrator ──query_erp──> analyst ──erp_list_invoices──> erp-mcp-iag:8889 → erp-mcp │ AuthZEN search/resource: │ which Invoice ids may this subject CAN_VIEW? └─> SELECT ... WHERE external_id = ANY() @ erp-db The graph decides through `Department -SERVES-> Customer -HAS_INVOICE-> Invoice` (staff) and the direct `HAS_INVOICE` edge (customers), backed by the `staff-can-view-invoice` KBAC policy. The payoff prompt is *"Show me the invoices"*: **same prompt, three row counts** - millicent (support + trading) sees all 9, leslie (support) 6, roy (trading) 3. The database knows nothing about the graph; connecting to Postgres directly shows every row unfiltered. - **Everything ships in the canbank dataset**: the `Invoice` nodes, `SERVES`/`HAS_INVOICE` edges, the KBAC policy, and the `wf-erp` / `wf-erp-analyst` / `wf-erp-console` workflow chains - provisioned by the same instant-stack run as chapter 5. - **To enable**: create the `indykiteagent-erp` IdP client, then in `.env.canbank` add `erp` to `COMPOSE_PROFILES`, append `,erp=http://erp-mcp-iag:8889/mcp` to `ANALYST_MCP_SERVER_URLS`, set `ERP_TOOL_ENABLED=true` and the `ERP_MCP_IDP_CLIENT_SECRET`, leave `ERP_WORKFLOW_ID` empty (unpinned, like Drive), and build with `make new-erp-mcp`. - **Trust boundary**: `erp-mcp` and `erp-db` are not published to the host - only `erp-mcp-iag` reaches them. The server reads the subject from the gateway-minted delegation token; exposing it directly would let a self-crafted token read other subjects' rows. Delegation into a real SaaS: Salesforce cases (compose profile `crm`) The `crm` profile completes the picture for any usecase: the delegation chain the gateways verify can land in a **real third-party SaaS**. A staff login asks the console to open a case; the CRM agent (an A2A agent behind `crm-iag`, workflow `wf-crm`) reads the `sub` and `act` chain from the gateway-minted delegation token, exchanges a signed JWT Bearer assertion (RFC 7523) for a Salesforce access token, and creates the Case with *"Filed on behalf of via agent chain …"* in its description. `wf-crm` is staff-only, so the same prompt from a customer login ends in the familiar red DENY - and the audit terminal shows two TOKEN cards per run: the IndyKite delegation token and the (redacted) Salesforce token. Like the ERP pair, the CRM agent's port is not published to the host - only `crm-iag` reaches it, so the on-behalf-of attribution written into Salesforce always rests on a gateway-verified token. To enable it: provision the `wf-crm` workflow in your dataset, create the `indykiteagent-crm` IdP client, set up a Salesforce Connected App with the JWT Bearer flow (digital signatures, `api` + `refresh_token/offline_access` scopes, pre-authorized profile), and fill the CRM entries in the usecase env file (`CRM_IDP_CLIENT_ID/SECRET`, `IAG_CRM_HOST`, `SF_CONSUMER_KEY`, `SF_USERNAME`, `crm` in `COMPOSE_PROFILES`). The orchestrator registers its `query_crm` tool when `CRM_HOST` is set. The repo's usecase `DEMO_SCRIPT.md` files carry the full setup walkthrough. ### Where you are now A third-party MCP server is running behind the Agent Gateway with per-user, graph-backed authorization and a full audit trail - and the MCP server itself never changed. The same recipe applies to any MCP Streamable HTTP server: wrap it if it speaks stdio (chapter 4), model who may reach it and through which chain (chapter 5), and point one gateway instance at it (chapter 6). --- Source: https://developer.indykite.com/tutorials/tutorial-agent-gateway-mcp --- # Protect agent-to-agent workflows with Agent Gateway > Learn what the Indykite Agent Gateway (IAG) does and how to deploy it in front of each agent of the iag-demo reference app. **Category:** Agent Gateway ## Summary Follow the chapters in order to understand IAG, model a workflow in the IKG, write the KBAC policy and ContX IQ query, configure the gateway, and run the iag-demo with three protected agents (orchestrator, retriever, weather). This tutorial is grounded in the iag-demo reference application. The demo runs a small canbank agentic workflow with three protected agents and one IAG instance in front of each. ### What you will have at the end - A clear mental model of how IAG validates caller, workflow, and delegation chain. - A `Workflow` node (`external_id=wf1`), three `Agent` nodes, and `INVOKES` relationships in the IKG. - A `CAN_TRIGGER` KBAC policy and a ContX IQ query that returns `(workflow, agent_list)` pairs. - Three IAG instances running via Docker Compose: `orchestrator-iag`, `retriever-iag`, `weather-iag`. - Auditable `AUTHORIZED` and `NOT_AUTHORIZED` records delivered to the chatbot webhook. ### Who this tutorial is for - Developers wiring up agents over the **A2A** protocol who need policy enforcement in front of each agent. - Platform and security engineers who want **traceable user-to-agent delegation**. - AI agents consuming this doc as a runbook - every chapter uses explicit service names, ports, file paths, and commands. ### Demo topology at a glance **Service** **Role** **Port** `chatbot` Web UI and A2A client; also receives audit webhooks. `3000` `orchestrator-iag` IAG in front of the orchestrator agent. `8881` `orchestrator` A2A orchestrator; delegates to retriever or weather. `6001` `retriever-iag` IAG in front of the retriever agent. `8882` `retriever` Answers canbank questions via MCP against the IKG. `6002` `weather-iag` IAG in front of the weather agent. `8884` `weather` Returns weather forecasts from Open-Meteo. `6004` ## Chapter 1: What is the Agent Gateway? Understand IAG's role in an A2A workflow using the iag-demo topology as a concrete example. ## Chapter 1: What is the Agent Gateway? The **Indykite Agent Gateway (IAG)** is a standalone service that protects one AI agent. Deploy one IAG per protected agent - in the iag-demo that means three instances: `orchestrator-iag`, `retriever-iag`, and `weather-iag`. For the full product reference, see the official Agent Gateway documentation. IAG talks to two backends: - The **IndyKite Platform** - for workflow data (IKG), KBAC/AuthZEN policies, and ContX IQ queries. - An external **OAuth2-compliant identity provider (IdP)** - for introspecting caller tokens, obtaining actor tokens, and exchanging them into delegated tokens. The demo uses `https://idsvr.indykite.one/oauth/v2/`. ### Not a regular proxy IAG protects two kinds of downstream, selected per instance by the `protected_agent.protocol` setting: - **A2A agents** (`protocol: a2a`, the default) - the case the iag-demo uses. IAG is not a generic reverse proxy: it tracks agent sessions so that A2A messages stream properly between Source and Target agents, and it appears as the **Target** from the caller's point of view and as a **Source** from the protected agent's point of view. - **MCP servers** (`protocol: mcp`) - IAG proxies MCP Streamable HTTP traffic (`initialize`, `notifications/initialized`, `tools/list`, `tools/call`) to a downstream MCP server, streaming SSE responses through without truncating them. Either way the authorization checks are identical - only the downstream protocol differs. A2A remains the default, so existing A2A deployments are unaffected. ### When to use IAG **Use case** **What IAG gives you** Protect A2A workflows A policy-controlled enforcement point in front of each protected agent. Protect an MCP server The same enforcement point in front of an MCP server (`protocol: mcp`), with SSE/streaming responses preserved. Explicit, traceable user-to-agent delegation Delegation validation through OAuth token exchange and the token's `act` chain, verified against workflow definitions in the IKG. Workflow-aware authorization Validates that the caller can trigger the workflow *and* that the requested sequence of agents is permitted. Auditable decisions outside the agent runtime Per-request records of who invoked which protected agent, when, and why a request was allowed or denied. ### Mental model (iag-demo) leslie (user) │ login via chatbot ▼ chatbot:3000 │ A2A JSON-RPC ▼ orchestrator-iag:8881 ── introspect / exchange ──▶ IdP (idsvr.indykite.one) │ ── CAN_TRIGGER wf1 ──▶ AuthZEN │ ── workflows / chains ──▶ ContX IQ (IKG) │ ── audit webhook ──▶ chatbot:3000/api/push-update ▼ orchestrator:6001 │ delegates ▼ retriever-iag:8882 ──▶ retriever:6002 (for canbank questions) weather-iag:8884 ──▶ weather:6004 (for weather questions) ### What comes next Chapter 2 walks through the exact sequence of checks IAG performs for every incoming A2A request. ## Chapter 2: How IAG handles a request at runtime The nine-step path a request takes through orchestrator-iag to the protected agent. ## Chapter 2: How IAG handles a request at runtime Every IAG instance in the iag-demo performs the same sequence. This chapter uses `orchestrator-iag` as the concrete example. ### The nine steps - **Receive** an A2A JSON-RPC request (for example `POST /v1/message/send`) from the chatbot on port `8881`. - **Introspect** the caller's token at the IdP's `oauth-introspect` endpoint. - **Obtain an actor token** for the protected agent via client credentials (`ORCHESTRATOR_IDP_CLIENT_ID` / `ORCHESTRATOR_IDP_CLIENT_SECRET`) at `oauth-token`. - **Exchange** the caller token and actor token for a delegated token (`oauth-token`). - **Query ContX IQ** with the configured `CIQ_QUERY_ID` to get the workflows the protected agent belongs to and the allowed agent chains for each. - **Check the subject**: call AuthZEN with action `CAN_TRIGGER` on each candidate `Workflow`. - **Check the chain**: confirm the requested `act` delegation chain matches an allowed `agent_list`. - **Forward** the request to `orchestrator:6001` with the delegated token. - **Return** the protected agent's response to the caller, and write an audit record. ### Where each question is answered **Step** **Question** **Service** Introspect Is the caller token valid and active? IdP Client credentials Can IAG authenticate as the protected agent? IdP Token exchange Can the caller delegate to the protected agent? IdP ContX IQ query Which workflows is this agent part of, and what chains are allowed? ContX IQ (IKG) AuthZEN Can the subject `CAN_TRIGGER` at least one candidate workflow? AuthZEN / KBAC Chain check Does the requested act chain match an allowed agent chain? IAG (in-process) ### HTTP responses **Code** **Meaning** `400` Bad request. The request cannot be processed because of its content. `401` Unauthorized. IAG cannot identify the caller. `403` Forbidden. Caller is authenticated but not allowed. `500` Internal error. Unexpected internal failure. `502` Bad gateway. Upstream or gateway-side processing issue. IAG may also translate upstream errors where appropriate (for example, a `404` returned by ContX IQ). ### Supported endpoints When `protected_agent.protocol` is `a2a` (the default, used by the iag-demo): - Any method on `/` for generic JSON-RPC (`message/send`, `tasks/get`). - `POST /v1/message:send` and `POST /v1/message/send` for A2A SendMessage. - `POST /v1/tasks:get` and `POST /v1/tasks/get` for A2A GetTask. When `protected_agent.protocol` is `mcp`, IAG proxies MCP Streamable HTTP JSON-RPC instead - `initialize`, `notifications/initialized`, `tools/list`, and `tools/call`. The `Mcp-Session-Id` header is forwarded transparently in both directions, and SSE response bodies are streamed through without being cut off. The nine-step authorization sequence above is unchanged; only the forwarded protocol differs. ## Chapter 3: Prerequisites and agent registration Exact list of tools, IdP clients, and IndyKite artifacts you need before running iag-demo. ## Chapter 3: Prerequisites and agent registration Gather the items in this chapter before touching configuration. The iag-demo expects all of them to exist. ### Tooling - **Docker** + **Docker Compose v2**. - **Python 3.11+** with a package manager - pipenv is recommended for the in-repo services. - A **POSIX** OS is preferred. - Optional: a **Gemini API key**, or a local **Ollama** instance reachable at `http://host.docker.internal:11434`. ### IdP clients Create one OAuth2 client per service. The demo uses four clients and maps them to these env vars: **Client** **Used by** **Env vars** `console` `chatbot` (login flow) `CHATBOT_IDP_CLIENT_ID` / `_SECRET` `indykiteagent` `orchestrator` `ORCHESTRATOR_IDP_CLIENT_ID` / `_SECRET` `indykiteagent-2` `retriever` `RETRIEVER_IDP_CLIENT_ID` / `_SECRET` `indykiteagent-3` `weather` `WEATHER_IDP_CLIENT_ID` / `_SECRET` Your IdP must support **token introspection**, **client credentials**, and **token exchange**. The chatbot client's redirect URL must match `http://${CHATBOT_HOST}:${CHATBOT_PORT}/auth/callback`, otherwise the login will fail with an OAuth redirect mismatch. ### IndyKite artifacts - An IndyKite project with the **canbank** graph data ingested. - A `Workflow` node with `external_id=wf1`. - A **ContX IQ** knowledge query - its GID or name populates `CIQ_QUERY_ID`. - A **Token Introspect** configuration - created as part of the canbank app setup. See the Token Introspect guide for details. - An **App Agent** whose credentials token populates `APP_AGENT_CREDENTIALS_TOKEN`. ### Register protected agents in two places Each protected agent is authenticated through the IdP using the **client credentials** flow, and each must exist as a digital twin in the IKG so IAG can evaluate workflows against it. - In the **IdP**: create the client (see the table above). - In **IndyKite**: pre-register and catalog the agent in the IKG, using a **URI-style identifier** as `external_id`. ### Pre-flight checklist - Four IdP clients exist and you have their IDs and secrets. - Canbank data is visible in your IKG. - `Workflow` with `external_id=wf1` exists and links to the three `Agent` nodes. - You have the ContX IQ query ID and the App Agent credentials token. - You can reach `${INDYKITE_BASE_URL}/contx-iq/v1` and `${INDYKITE_BASE_URL}/access/v1`. ## Chapter 4: Model the wf1 workflow in the IKG The Workflow, Agent nodes, and INVOKES relationships that back the iag-demo canbank workflow. ## Chapter 4: Model the wf1 workflow in the IKG IAG validates whether the requested `act` delegation chain matches a workflow-defined agent chain. The iag-demo uses a single workflow with `external_id=wf1` and three agents. ### Required data shape - A `Workflow` node identified by `external_id` - here, `wf1`. - `Agent` nodes identified by `external_id` - `orchestrator`, `retriever`, `weather`. - `INVOKES` relationships between agents. - A `workflow_name` property on each `INVOKES` - set to `wf1` for this demo. ### Demo workflow graph (Workflow {external_id: "wf1"}) (Agent {external_id: "orchestrator"}) -[INVOKES {workflow_name: "wf1"}]-> (Agent {external_id: "retriever"}) (Agent {external_id: "orchestrator"}) -[INVOKES {workflow_name: "wf1"}]-> (Agent {external_id: "weather"}) ### Why `workflow_name` is mandatory An agent may participate in several workflows. The `workflow_name` property on the `INVOKES` relationship identifies which workflow a given invocation belongs to. The ContX IQ query in Chapter 5 only returns relationships whose `workflow_name` matches the `Workflow.external_id`, so chains without this property are silently excluded. ### Valid delegation chains in wf1 chatbot -> orchestrator -> retriever chatbot -> orchestrator -> weather Any chain that skips the orchestrator (for example `chatbot -> retriever`) or adds an agent not modeled in wf1 is rejected with `403 Forbidden`. ### How to capture the data Use whichever ingestion path you already use in your project: - The **Capture API** (`POST /capture/v1/nodes` and `POST /capture/v1/relationships`). - The **IndyKite Hub** UI. - An existing Terraform or identity pipeline. The `bruno/iag-demo` folder of the iag-demo repo contains ready-made sample requests you can replay against your project. ### Common pitfalls - **Missing `workflow_name`**: the ContX IQ query returns nothing and every request is denied. - **Identifier mismatch**: the `external_id` on each `Agent` must match the identifier carried in the `act` chain and used in the IdP. - **No link between the subject and the workflow**: even with a correct chain, the request is denied at the AuthZEN step if the subject cannot `CAN_TRIGGER` `wf1`. ## Chapter 5: Create the KBAC policy and ContX IQ query The CAN_TRIGGER policy and knowledge query that wire the iag-demo subject User to the wf1 workflow. ## Chapter 5: Create the KBAC policy and ContX IQ query IAG needs two authorization artifacts: - A **KBAC / AuthZEN policy** that answers "can this subject trigger this workflow?". - A **ContX IQ query** that returns, for the protected agent, the workflows it belongs to and the allowed agent chains inside each. ### Subject type used by iag-demo The demo configures `JARVIS_AUTHZEN_SUBJECT_TYPES=User`, so the KBAC policy below uses `User` as the subject type. If you add more subject types, create one policy per type and list them all in `JARVIS_AUTHZEN_SUBJECT_TYPES` (or `authzen.subject_types` in a YAML config). ### KBAC / AuthZEN policy { "meta": { "policy_version": "2.0-kbac" }, "subject": { "type": "User" }, "actions": [ "CAN_TRIGGER" ], "resource": { "type": "Workflow" }, "condition": { "cypher": "MATCH (subject)-[:CAN_TRIGGER]->(resource:Workflow)" } } This policy assumes your IKG contains a relationship like: `(:User)-[:CAN_TRIGGER]->(:Workflow {external_id: "wf1"})` In the canbank demo, users `leslie`, `roy`, and `rebecca` each have this relationship to `wf1`. ### ContX IQ query The query returns one row per `(workflow, agent_list)` pair for the protected agent identified by `$agent_id`: { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "_Application" }, "condition": { "cypher": "MATCH (subject:_Application) MATCH (wf:Workflow)-[rels:INVOKES*]->(a:Agent {external_id: $agent_id}) WHERE ALL(r IN rels WHERE r.workflow_name = wf.external_id AND endNode(r):Agent) WITH subject, wf.external_id AS workflow, [r IN rels | endNode(r).external_id] AS agent_list", "filter": [] }, "allowed_reads": { "nodes": [], "relationships": [], "aggregate_values": [ "workflow", "agent_list" ] } } Record the query GID or name - it populates `CIQ_QUERY_ID` in your `.env`. ### Enforced response shape IAG expects the response in this exact shape: { "data": [ { "aggregate_values": { "agent_list": ["orchestrator", "retriever"], "workflow": "wf1" } }, { "aggregate_values": { "agent_list": ["orchestrator", "weather"], "workflow": "wf1" } } ] } In iag-demo, the same `wf1` workflow appears twice - once per allowed chain. ### How IAG combines the two - IAG runs the ContX IQ query and gets the candidate `(workflow, agent_list)` pairs. - For each `workflow`, IAG asks AuthZEN whether the subject has `CAN_TRIGGER`. - If at least one workflow is triggerable *and* the requested delegation chain matches its `agent_list`, IAG forwards the request. If either check fails, IAG responds with `403` and the audit record's `decision` is `NOT_AUTHORIZED`. ## Chapter 6: Configure IAG: YAML fields and the iag-demo env vars Both ways to configure IAG - a full config.yaml and the JARVIS_* environment variables used by iag-demo. ## Chapter 6: Configure IAG IAG accepts either a YAML config file passed via `--config=/app/config.yaml` or a set of `environment variables`. The iag-demo uses the **environment-variable** form so a single shared file (`iag-base-docker.yaml`) can be reused by three services. ### Configuration sections The configuration is grouped by concern. The same keys appear in both forms - YAML fields use dots (`service.name`), and environment variables use the `JARVIS_` prefix with underscores (`JARVIS_SERVICE_NAME`). **Section** **Purpose** `service` Runtime: `name`, `port`, `environment`, `log_level`. `identity_provider` IdP `base_url` and endpoints: `introspect_endpoint`, `client_credential_endpoint`, `exchange_endpoint`. `protected_agent` Target `base_url`, the downstream `protocol` (`a2a` default, or `mcp`), and its client credentials (`authentication.client_id`, `authentication.client_secret`, `authentication.type=credentials`). `authzen` `base_url`, `action` (typically `CAN_TRIGGER`), `subject_types`, and cache tuning. `contx_iq` `base_url`, `query_id`, `app_agent_credentials_token`, optional `allowed_workflow_id`, and cache tuning. `audit` Optional. Either `delivery: webhook` (with `http.url`, `http.method`, `http.auth`) or `delivery: file` (with `storage_path`, `format`, `rotation_strategy`). ### Defaults worth knowing - `protected_agent.protocol` defaults to `a2a`; set it to `mcp` to put IAG in front of an MCP server. Any other value fails startup with *invalid protected_agent protocol*. As an environment variable this is `JARVIS_PROTECTED_AGENT_PROTOCOL`. - Caches (`cache_ttl`, `cache_update_after`) default to `5m`; `cache_update_after_error` defaults to `10s`. - Audit file rotation defaults: `rotation_interval=24h`, `rotation_max_bytes=100MiB`. - Webhook API key auth defaults `api_key_header` to `X-API-Key`. ### Form 1 - Full `config.yaml` --- service: name: agent-gateway port: 1234 environment: prod log_level: debug identity_provider: base_url: http://idp.example.com:1234 introspect_endpoint: /token/introspect client_credential_endpoint: /token exchange_endpoint: /token/exchange protected_agent: base_url: http://agent.example.com:5678 protocol: a2a # a2a (default) or mcp authentication: type: credentials client_secret: secret-password client_id: idp-actor-client-id authzen: base_url: https://us.api.indykite.com/access/v1 action: CAN_TRIGGER subject_types: - Person - User - Agent cache_ttl: 5m cache_update_after: 5m cache_update_after_error: 10s contx_iq: base_url: https://us.api.indykite.com/contx-iq/v1 query_id: gid:abc123def456 app_agent_credentials_token: jwtheader.payload.signature cache_ttl: 5m cache_update_after: 5m cache_update_after_error: 10s allowed_workflow_id: my-awesome-workflow audit: delivery: webhook http: url: https://example.com:9090/audit method: POST auth: type: mTLS mtls_certificate_file_path: cert.pem mtls_private_key_path: key.pem ### Form 2 - `environment variables` (iag-demo) The iag-demo shares a single base service via `iag-base-docker.yaml`. Each IAG instance extends it and overrides only the per-agent values. # iag-base-docker.yaml (excerpt, verbatim from the demo) services: iag-base: image: indykite/agent-gateway:latest environment: JARVIS_SERVICE_LOG_LEVEL: debug JARVIS_SERVICE_ENVIRONMENT: demo JARVIS_IDENTITY_PROVIDER_BASE_URL: "https://idsvr.indykite.one/oauth/v2/" JARVIS_IDENTITY_PROVIDER_INTROSPECT_ENDPOINT: "oauth-introspect" JARVIS_IDENTITY_PROVIDER_CLIENT_CREDENTIAL_ENDPOINT: "oauth-token" JARVIS_IDENTITY_PROVIDER_EXCHANGE_ENDPOINT: "oauth-token" JARVIS_CONTX_IQ_BASE_URL: ${INDYKITE_BASE_URL}/contx-iq/v1 JARVIS_CONTX_IQ_QUERY_ID: ${CIQ_QUERY_ID} JARVIS_CONTX_IQ_APP_AGENT_CREDENTIALS_TOKEN: ${APP_AGENT_CREDENTIALS_TOKEN} JARVIS_CONTX_IQ_ALLOWED_WORKFLOW_ID: ${WORKFLOW_ID} JARVIS_AUTHZEN_BASE_URL: ${INDYKITE_BASE_URL}/access/v1 JARVIS_AUTHZEN_ACTION: CAN_TRIGGER JARVIS_AUTHZEN_SUBJECT_TYPES: User extra_hosts: - "host.docker.internal:host-gateway" ### Per-agent overrides in the demo Each IAG service in `docker-compose.yaml` sets its own `JARVIS_SERVICE_NAME`, `JARVIS_SERVICE_PORT`, `JARVIS_PROTECTED_AGENT_BASE_URL`, `JARVIS_PROTECTED_AGENT_AUTHENTICATION_CLIENT_ID`, and `JARVIS_PROTECTED_AGENT_AUTHENTICATION_CLIENT_SECRET`. For example: **IAG instance** **Protected agent URL** **Client ID env var** `orchestrator-iag` (`:8881`) `http://orchestrator:6001` `ORCHESTRATOR_IDP_CLIENT_ID` `retriever-iag` (`:8882`) `http://retriever:6002` `RETRIEVER_IDP_CLIENT_ID` `weather-iag` (`:8884`) `http://weather:6004` `WEATHER_IDP_CLIENT_ID` ### Audit configuration in the demo The demo ships a minimal `audit-config.yaml` that every IAG mounts and points all three gateways at the chatbot's webhook endpoint: audit: delivery: webhook http: url: http://chatbot:3000/api/push-update method: post auth: type: no-auth That is why audit decisions appear in the chatbot UI in real time. ## Chapter 7: Deploy: iag-demo with Docker Compose, and Kubernetes variants Build the in-repo services, pin the IAG image, bring up the compose stack, and see how the same wiring maps to Kubernetes standalone and sidecar patterns. ## Chapter 7: Deploy IAG IAG is delivered as a Docker image. **Deploy one IAG instance per protected agent**, regardless of topology. The iag-demo uses three instances: `orchestrator-iag`, `retriever-iag`, `weather-iag`. ### Step-by-step: run the iag-demo - **Clone** the repo and enter `a2a/iag-demo`. **Copy the env file**: `cp .example.env .env` - **Fill in the required variables**: `INDYKITE_BASE_URL`, `CIQ_QUERY_ID`, `WORKFLOW_ID=wf1`, `APP_AGENT_CREDENTIALS_TOKEN`, and the four `*_IDP_CLIENT_ID` / `*_IDP_CLIENT_SECRET` pairs. **Generate a Flask secret**: `python -c "import secrets; print(secrets.token_hex(32))"` Paste the output into `FLASK_SECRET_KEY` in `.env`. **Build the in-repo services**: `make` This builds the chatbot, orchestrator, retriever, and weather images. **Pin the IAG image**. In `iag-base-docker.yaml`, replace `latest` with a specific tag - if you leave it as `latest` Docker may fail with *manifest not found*: `image: indykite/agent-gateway:1.783.1` **Start the stack**: `docker compose up` - **Open** `http://localhost:3000` and log in as a demo user (`leslie`, `roy`, or `rebecca`). ### Tail the gateway logs When a request is denied, look here first: `docker compose logs -f orchestrator-iag retriever-iag weather-iag` ### Shut down `docker compose down` ### Option: run a single IAG with `docker run` Outside the demo, a single IAG instance can be started with one command and a `config.yaml`: docker run -d \ --name agent-gateway \ -p : \ --add-host host.docker.internal:host-gateway \ -v $(pwd)/config.yaml:/app/config.yaml \ -v $(pwd)/audit:/app/audit \ indykite/agent-gateway:latest \ --config=/app/config.yaml The Docker user must have read permission on `config.yaml`. ### Kubernetes - standalone Pod The gateway and the protected agent run in different Pods. Use this when they have independent lifecycles or scaling. - A **Deployment** for IAG (single container, `indykite/agent-gateway:`). - A **Service** to expose the gateway internally (and externally if needed). - A **ConfigMap** and/or **Secret** mounted as `/app/config.yaml`. - An optional **Ingress** or **LoadBalancer**. Set `protected_agent.base_url` to the in-cluster DNS name, for example: `http://protected-agent..svc.cluster.local:8080` ### Kubernetes - sidecar IAG and the protected agent run in the same Pod and share a network namespace. Traffic between them stays on `localhost`. - One **Deployment** with two containers: `agent-gateway` and `protected-agent`. - A **Service** that exposes only the gateway port. Set `protected_agent.base_url` to the local instance so traffic stays inside the Pod: `http://127.0.0.1:` ### Configuration management tips - Simple setups: store the whole `config.yaml` in a **Secret**. - Stricter setups: split non-sensitive values into a **ConfigMap**, credentials into a **Secret**, combine them at runtime. - The cluster must allow **egress** to the audit endpoint or webhook delivery will fail. - Use **NetworkPolicy** to constrain traffic between gateway, protected agent, IdP, and IndyKite. ### Troubleshooting quick reference **Symptom** **Likely cause** *manifest not found* on `docker compose up` `latest` tag in `iag-base-docker.yaml`; pin a real version. OAuth redirect mismatch at login IdP client redirect URL doesn't match `http://${CHATBOT_HOST}:${CHATBOT_PORT}/auth/callback`. `401` from a gateway Caller token can't be introspected. Check the IdP and the token. `403` from a gateway Subject lacks `CAN_TRIGGER` on `wf1`, or the CIQ query returns no matching chain. ## Chapter 8: Exercise iag-demo and read the audit logs Send real prompts through the canbank chatbot, watch IAG decisions in real time, and interpret audit records in CSV, JSON, and TXT formats. ## Chapter 8: Exercise iag-demo and read the audit logs **Prerequisite:** complete Chapter 7 so the full Docker Compose stack is up. This chapter assumes `docker compose up` is running and all three IAG instances are healthy. Log in at `http://localhost:3000` as `leslie`. Send the prompts below and watch the audit messages appear - the chatbot UI receives them on `/api/push-update`. ### Demo users The canbank dataset ships with three personas. They all have `CAN_TRIGGER` on `wf1`, so any of them can log into the chatbot. The scenarios differ: **User** **Role** **Use when you want to...** `leslie` Customer Service Rep (CSR 2) Ask policy and past-decision questions on behalf of customers. Default login for the refund-policy flow. `roy` Retail trader Exercise trader-side prompts from a non-CSR perspective. `rebecca` Customer with a credit card and a trading account Be the subject of holdings queries (for example, NVDA shares Rebecca can purchase). Can also log in directly as the customer. ### Sample prompts and expected routes **Prompt** **Routed through** **Expected decision** "What policy documents pertain to refunds?" `chatbot → orchestrator → retriever` `AUTHORIZED` on `orchestrator-iag` and `retriever-iag`. "Retrieve past decisions that incorporated the 'refund_policy' document." `chatbot → orchestrator → retriever` `AUTHORIZED`. "Tell me how many shares of NVDA the user with id `rebecca` can purchase." `chatbot → orchestrator → retriever` `AUTHORIZED` if `leslie` has `CAN_TRIGGER` on `wf1`. "What's the weather in London?" `chatbot → orchestrator → weather` `AUTHORIZED` on `orchestrator-iag` and `weather-iag`. ### Make IAG reject requests on purpose Each of these should end with `403 Forbidden` and a `NOT_AUTHORIZED` audit record: - **Skip the orchestrator**: call `retriever-iag` (`:8882`) directly without the orchestrator in the `act` chain. Chain `chatbot -> retriever` is not in `wf1`. - **Break the graph**: remove the `workflow_name` property from one `INVOKES` relationship and retry - the ContX IQ query no longer returns that chain. - **Remove the subject link**: delete the `(:User {external_id:"leslie"})-[:CAN_TRIGGER]->(:Workflow {external_id:"wf1"})` edge. AuthZEN now says no. - **Wrong subject type**: log in with a user whose type is not `User`. It is not listed in `JARVIS_AUTHZEN_SUBJECT_TYPES`, so no policy matches. ### Service logs vs audit logs Service logs are written in JSON to standard output (`docker compose logs -f orchestrator-iag`). Audit messages are a separate stream and can be delivered as: - A **webhook** - what the iag-demo uses, pointed at `http://chatbot:3000/api/push-update`. - A **file** in CSV, JSON, or TXT format with size-, time-, or size-and-time-based rotation. ### Audit formats Webhook body (and JSON file) { "decision": "AUTHORIZED", "reason": "subject can trigger workflows wf1 and the actors in the delegation chain", "subject": "leslie", "actor": "orchestrator", "action": "invoke", "service": "orchestrator-iag", "timestamp": "2026-01-02T15:04:05.999999999Z07:00", "traceID": "c323b688-3c01-4559-838d-59ac7a81ee1a" } CSV file decision,reason,subject,actor,action,service,timestamp,traceID AUTHORIZED,'subject can trigger workflows wf1 and the actors in the delegation chain',leslie,orchestrator,invoke,orchestrator-iag,2026-01-02T15:04:05.999999999Z07:00,c323b688-3c01-4559-838d-59ac7a81ee1a NOT_AUTHORIZED,no workflow matches the actors chain,leslie,retriever,invoke,retriever-iag,2026-01-02T15:04:06.999999999Z07:00,2d17528b-5005-4f5b-8d39-5207c394ae9b TXT file `[2026-01-02T15:04:05.999999999Z07:00] decision=AUTHORIZED reason=subject can trigger workflows wf1 and the actors in the delegation chain subject=leslie actor=orchestrator action=invoke service=orchestrator-iag traceID=c323b688-3c01-4559-838d-59ac7a81ee1` ### Reading an audit record **Field** **What to check** `decision` `AUTHORIZED` or `NOT_AUTHORIZED`. The binary outcome. `reason` Human-readable explanation; useful for diagnosing failed requests. `subject` / `actor` Who initiated the request and which protected agent was targeted. `service` Which IAG wrote the record - `orchestrator-iag`, `retriever-iag`, or `weather-iag`. `traceID` Correlate with protected-agent logs and upstream client traces. ### Where to go next - Add a fourth agent to `wf1` and watch the allowed chains grow in the ContX IQ response. - Switch audit delivery from the chatbot webhook to your SIEM. - Migrate from `ENV VARS` to a full `config.yaml` for environments that require stricter configuration management. - Move to Kubernetes: start with standalone Pods for independent scaling, or go sidecar for tighter coupling between each agent and its IAG. --- Source: https://developer.indykite.com/tutorials/tutorial-agent-gateway --- # Build IndyKite end-to-end with the music dataset > Download the music-dataset Postman collection and follow the chapters to set up an IndyKite environment, ingest a music graph, and wire up KBAC and ContX IQ policies, queries, and executes. **Category:** ContX IQ ## Summary Follow the chapters in order to import the music collection, create the IndyKite environment, ingest 16k+ nodes and 31k+ relationships, then author and execute KBAC and ContX IQ policies and queries against the music graph. **Download the Postman collection - useful whether or not you used the Sandbox:** - **If you didn't spin up your environment from the Sandbox** - the collection bootstraps everything from scratch over REST: project, application, application agent + credentials, Token Introspect, MCP server, the full music IKG (16k+ nodes, 31k+ relationships), all KBAC policies, and all ContX IQ policies + Knowledge Queries. - **If you did spin it up from the Sandbox** - the collection is still your reference and workbench. Every request documents the exact REST shape of how the environment is wired, and it ships ready-to-run requests to update graph data via Capture, execute Knowledge Queries through ContX IQ, and make AuthZEN *is-authorized* evaluations against your live environment. music-dataset.postman_collection.json (Postman Collection format v2.1.0, ~18 MB - includes the inline music dataset). The **Music app** - the collection plus all the data behind it (nodes, relationships, policies, queries, and executes needed to recreate the whole environment) - can be cloned from github.com/indykite/developer-hub/music - it's also a runnable Flask web app that can click through this whole tutorial for you (see "The Music app" section below). The Person-subject flows are tuned for one end user, **Millicent Contextsworth** - the `ciq exec` requests carry her example `input_params`. You can retarget them to any other Person by changing those `input_params` in the execute requests. You'll mint her user token (via toolbelt.indykite.com) in Chapter 10. Import it into Postman, Bruno, Insomnia, or Newman - any client that parses the Postman Collection v2.1.0 format. The "Variables" tab lists every value the requests use; the only ones you must set yourself are `your_sa_token`, `your_agent_token`, `your_user_token`, and `organization_gid`. Everything else is auto-captured by the bundled post-response script. **Where to find each:** - `organization_gid` - in the IndyKite Hub under `/settings`. - `your_sa_token` - in the credentials file you downloaded at Sandbox registration, or in the Hub under `/service-accounts`. - `your_agent_token` - in the credentials file from Sandbox registration if you reuse that Sandbox env (the token only works for that env); if you create a new env, it's generated when you run the Postman collection / follow this tutorial. - `your_user_token` - covered later in this tutorial. This tutorial walks through that collection. Every chapter maps to a folder of requests inside it - run the requests in order and the collection variables fill themselves in via the bundled capture script. ### What you will have at the end - An IndyKite **Project**, **Application**, **Application Agent**, and credentials provisioned via the Config API. - A **Token Introspect** configuration that links your external IdP (Auth0) tokens to `Person` nodes in the IKG. - An **MCP Server** configuration so AI agents can call IndyKite over MCP. - A music **IKG** populated with the 6 node types and 13 relationship types listed below. - **10 KBAC policies** covering venue performance, entry, flash mobs, track-loudness checks, playlist sharing, family access, and DJ rights, plus **10 single AuthZEN evaluations** and a **boxcar batch**. - **24 ContX IQ policies** with **44 Knowledge Queries** (read / write / delete variants) and **44 paired CIQ executes** that exercise every query against the music graph. ### The music dataset at a glance After Chapter 5 (data ingestion) your IKG holds: **Node type** **Count** **Sample external_id** Artist94`artist-1` (38 Special) Album1k+`album-1` Track14k+`track-1` Playlist20`playlist-1` Person86 (12 with `auth0|...` external_ids)`person-1` / `auth0|69c3ee8cb9ed562744ff9326` Venue24`venue-1` (Shower-Concert-Hall) Relationships: `CREATED`, `PART_OF`, `RELEASED`, `LIKES`, `FOLLOWS`, `SUBSCRIBED_TO`, `MARRIED_TO`, `PARENT_OF`, `PARTNERS`, `CO_PARENTS`, `WILL_ATTEND`, `APPROVED_FOR`, `PLAYED_AT`. ### Who this tutorial is for - Developers evaluating IndyKite who want a runnable, self-contained walkthrough that touches every product surface (Capture, KBAC/AuthZEN, ContX IQ, Token Introspect, MCP). - Platform engineers preparing a demo environment for stakeholders. - AI agents using the collection as a runbook - every chapter names the exact request items and the variables they read/write. ### Prerequisites - An IndyKite Hub account (EU or US). See the Environment guide. - A ServiceAccount credential at the Organization level (provides the `{{your_sa_token}}` Bearer used on Config API requests). - Postman, Bruno, Insomnia, or Newman - any client that parses the Postman Collection format v2.1.0. Import the JSON file linked at the top of this page. - An end-user access token from your IdP (e.g. Auth0) for Chapter 10 - the Person-subject CIQ executes send it as `Authorization: Bearer {{your_user_token}}`. ### How to use the collection alongside this tutorial - **Download & import** the collection (link at the top of this page) into Postman. The collection's "Variables" tab shows every variable you might need. - Set `your_sa_token` and `organization_gid` manually. Everything else is auto-captured by the collection's post-response script as you run create requests. - Run requests **in document order**. The capture script writes each created resource's `id` into the matching variable (`project_gid`, `application_gid`, `app_agent_gid`, `policy_gid`, `query_gid`, ...) so the next request can use it. - The chapters below explain each section. Skip ahead if you only care about one product, but the variable chain only works left-to-right. ### The Music app: a clickable alternative to the collection If you'd rather click through the setup than fire REST requests one by one, the **Music app** (github.com/indykite/developer-hub/music) wraps the entire collection in a small Flask web app. Every request in the collection has a matching form page, and each successful create saves its resulting ID into a local `.env` file so the next step can use it - the same variable chain as the Postman capture script, just persisted on disk. **Run it locally:** - Clone the repo and `cd music`. - Create a `.env` file with the only values you type in yourself: `SA_TOKEN` (from the Hub under `/service-accounts`), `URL_ENDPOINTS` (`https://eu.api.indykite.com` or `https://us.api.indykite.com`), `ORGANIZATION_ID` (Organization > Settings), and optionally `USER_TOKEN` (an Auth0 ID token - only needed for the Person-subject CIQ executes and `/chat/`). Everything else (`PROJECT_ID`, `APP_TOKEN`, all the `*_ID` keys, ...) is saved automatically as you go. - Install pipenv, then run `pipenv install`, `pipenv shell`, and `flask run`; open the local URL (e.g. `http://127.0.0.1:5000`). **Getting Started steps 1-5.** The five cards on the landing page mirror Chapters 2-4 of this tutorial - each needs the values saved by the previous steps and saves its own result to `.env`: **#** **Step** **Page** **Saves to `.env`** 1Create Project`/api_project/create``PROJECT_ID` 2Create Application`/api_application/create``APPLICATION_ID` 3Create App Agent`/api_app_agent/create``APP_AGENT_ID`, `APP_TOKEN` 4Token Introspect`/api_token_introspect/create``TOKEN_INTROSPECT_ID` 5MCP Server`/api_mcp_server/create``MCP_SERVER_ID` **One-click provisioning.** Once steps 1-5 exist, **Provision Everything** (`/api_provision/run`) replays every remaining create button in click order: both captures (nodes and relationships, Chapter 5), the 10 KBAC policies (Chapter 6), then each CIQ policy followed by its Knowledge Queries (Chapters 8-9) - 93 steps, saving the same IDs to `.env` as clicking by hand. It's safe to re-run: steps whose ID is already saved are skipped. AuthZEN evaluations and CIQ executes are *not* included - they are reads, not creations - so Chapters 7 and 10 remain yours to run, either page by page or via the app's `/chat/` story mode, which runs every CIQ execute in dependency-safe order (creates before reads, deletes last). **Backfill `.env` - when the sandbox already exists.** If your music environment was created *outside* the app - an automated Hub sandbox, this tutorial's Postman collection, or scripts - the app's forms won't know any of the IDs. **Backfill .env** (`/api_provision/backfill`) recovers every derived ID by looking the configs up **by name** and saves them to `.env`, so the forms work against that existing sandbox. It is read-only: nothing is created or modified on the platform. - Needs only `URL_ENDPOINTS`, `SA_TOKEN`, and `PROJECT_ID` in `.env`. - Recognizes both the app's fixed config names and the console sandbox's random-suffixed names (`-123456-0`, `app-123456`, ...). - Tokens (`APP_TOKEN`, `USER_TOKEN`) cannot be recovered - credential secrets are only shown at creation - so set those manually. - Hub console sandboxes include an MCP server, so `MCP_SERVER_ID` is recovered along with the rest. If backfill reports any config as *missing*, create it afterwards with the matching Getting Started step. ### What comes next Chapter 1 introduces the music graph itself - what each node type means and which relationships connect them - so the policies and queries in later chapters land on a familiar shape. ## Chapter 1: The music graph: nodes, relationships, and scenarios Walk through the music dataset's six node types and thirteen relationship types so the rest of the tutorial lands on a familiar shape. ## Chapter 1: The music graph Every policy and query in this tutorial runs against the same Identity Knowledge Graph (IKG). Understanding the graph first makes the rest of the chapters self-evident. ### Why a graph? From the graph database guide: identity and authorization are inherently relational. "Can *this* person play *that* track at *this* venue" is a path query, not a row lookup. The IKG stores entities as nodes and the connections between them as relationships, so KBAC and ContX IQ can reason over those paths directly. ### Node types **Type** **What it represents** **Key properties used by policies / queries** `Person`User of the music app. 12 use real `auth0|...` external_ids (so a real IdP-issued bearer's `sub` claim resolves to a graph node); the other 74 use `person-N`. Two auth0 users are seeded with engagement edges as recommended test subjects: **Cornelius** (`auth0|69c3ee8cb9ed562744ff9326`) covers music-engagement queries; **Marmaduke** (`auth0|69c4129b0ba356c7db9f91ab`) covers parent-side queries. Full table in Chapter 5.`firstname`, `lastname`, `city`, `music_mood`, `karaoke_confidence`, `dance_skill`, `profession` `Artist`Musician or group.`name` `Album`Released by an Artist; contains Tracks.`title` `Track`Individual song with audio characteristics used in policies (e.g. loudness gates).`title`, `duration`, `danceability`, `energy`, `loudness`, `popularity` `Playlist`User-created or system playlist. Has venue approvals and subscriber relationships.`name` `Venue`Place where tracks are played. Carries thresholds policies compare against.`name`, `min_confidence`, `min_danceability`, `max_loudness`, `max_energy` ### Relationships **Relationship** **From → To** **Used by** `CREATED`Artist → Track, Person → PlaylistCatalog ownership / playlist authorship `PART_OF`Track → AlbumCatalog hierarchy `RELEASED`Artist → AlbumCatalog ownership `LIKES`Person → TrackSame-taste matching, popular-tracks queries `FOLLOWS`Person → ArtistSame-artist-followers query `SUBSCRIBED_TO`Person → PlaylistSubscription read / share KBAC `MARRIED_TO` / `PARTNERS` / `PARENT_OF` / `CO_PARENTS`Person ↔ PersonFamily-playlist KBAC, children-likes CIQ `WILL_ATTEND`Person → VenueConcert / venue-attendees / DJ KBAC `APPROVED_FOR`Playlist → VenuePlaylist-venue-approval KBAC `PLAYED_AT`Track → VenueVenue-playable-tracks CIQ, track-loudness KBAC ### The scenarios you'll author Three families of authorization questions are answered in later chapters: - **KBAC / AuthZEN** (Chapters 6 - 7): yes/no decisions like "can *person-52* PERFORM at *venue-1*?" or "can *track-7431* PLAY at *venue-4*?" - **ContX IQ - Application as subject** (Chapter 8 onwards): system-level reads/writes like "list playable tracks for venue X" run by the `_Application` identity. - **ContX IQ - Person as subject** (Chapter 8 onwards): user-scoped reads/writes like "my profile", "my playlists", "people with the same taste as me", scoped by `subject.external_id = $token.sub` (i.e. anchored to the bearer token's verified `sub` claim - no client-supplied identity). ### What comes next Chapter 2 sets up the IndyKite environment that owns this graph - Project, Application, Application Agent, and credentials. ## Chapter 2: Set up the IndyKite environment Create Project, Application, Application Agent, and credentials with the four Config API requests in the music collection. ## Chapter 2: Set up the IndyKite environment From the Environment guide: before you can use any IndyKite capability you need an **Organization → Project → Application → Application Agent** hierarchy. The collection's first four requests build that chain. Each one is auto-captured into a collection variable consumed by the next. ### Hierarchy refresher **Level****Why it exists** **Organization**Top-level account, holds Service Accounts and Projects. **Project**Isolated environment with its own IKG. **Application**Logical grouping inside a Project. Creates an `_Application` node so the application can act as a subject. **Application Agent**API authentication identity. Its credentials are sent as `X-IK-ClientKey`. ### Variables you set yourself - `your_sa_token` - the ServiceAccount JWT, sent as `Authorization: Bearer ...` on every request in this chapter. - `organization_gid` - your Organization's `gid:...` identifier. ### Run these requests in order **#** **Request name** **Endpoint** **Captures into** 1`create project``POST /configs/v1/projects``project_gid` 2`create application``POST /configs/v1/applications``application_gid` 3`create application agent``POST /configs/v1/application-agents``app_agent_gid` 4`create application agent credentials``POST /configs/v1/application-agent-credentials`(see below) ### Request bodies **1. Create the Project.** Auth: `Authorization: Bearer {{your_sa_token}}`. POST /configs/v1/projects { "organization_id": "{{organization_gid}}", "name": "eu-project", "display_name": "EU Project", "region": "europe-west1", "ikg_size": "2GB" } **2. Create the Application** inside that project. POST /configs/v1/applications { "project_id": "{{project_gid}}", "name": "app-name", "display_name": "Application name", "description": "Application description" } **3. Create the Application Agent.** The `api_permissions` array is what makes this agent able to call Capture, ContX IQ, AuthZEN, etc. Drop any you don't need. POST /configs/v1/application-agents { "application_id": "{{application_gid}}", "name": "app-agent-name", "display_name": "App Agent name", "description": "App Agent description", "api_permissions": [ "Authorization", "Capture", "ContXIQ", "EntityMatching", "IKGRead", "ReadDataSchema" ] } **4. Generate credentials** for that agent. The response is the credential JSON you'll mine for `your_agent_token`. POST /configs/v1/application-agent-credentials { "application_agent_id": "{{app_agent_gid}}", "display_name": "AppAgent Credentials name", "expire_time": "2027-04-28T12:34:56Z" } ### A note on credentials (step 4) From the Credentials guide: the response of the credentials endpoint contains a downloadable JSON. Inside that JSON is the value used as `X-IK-ClientKey`. The collection's auto-capture script logs a hint when this request fires - you must **copy the X-IK-ClientKey value into `your_agent_token` manually**. Save the credentials file securely; it cannot be retrieved again. ### BYODB alternative The collection also includes `create project BYODB` for users who want to bring their own Neo4j database. Set `db_name`, `db_password`, and `db_host` in the Variables tab before running it. Most users should use the managed-IKG `create project` path instead. ### Sanity check After running these four requests, your collection variables tab should show non-empty values for `project_gid`, `application_gid`, `app_agent_gid`, and `your_agent_token` (the last one filled in manually from the credential JSON). ### What comes next Chapter 3 wires Token Introspect into your environment so external IdP tokens can be validated against the IKG. ## Chapter 3: Configure Token Introspect for end-user identity Link external IdP access tokens (Auth0) to Person nodes in the IKG so user-context CIQ executes resolve to a real graph subject. ## Chapter 3: Configure Token Introspect From the Token Introspect guide: Token Introspect tells IndyKite how to validate an external access token (issued by your IdP) and which claim should be used to find the matching `Person` node. Without it, Person-subject CIQ executes have no way to know which graph node "the caller" refers to. ### Why this matters for the music collection In Chapter 5 you will ingest 12 `Person` nodes whose `external_id` values are real Auth0 subjects (e.g. `auth0|69c3ee8cb9ed562744ff9326`). The Person-subject CIQ executes you'll run in Chapter 10 send those tokens as `Authorization: Bearer {{your_user_token}}`. Token Introspect is what makes the platform resolve the bearer token's `sub` claim to the matching `Person` node. ### Run this request **Request** **Endpoint** **Captures into** `create token introspect``POST /configs/v1/token-introspects``token_introspect_gid` ### Auth: ServiceAccount Like all `/configs/v1/*` requests, this one uses `{{your_sa_token}}` as `Authorization: Bearer ...`. No agent token here - this is a configuration call. ### Body POST /configs/v1/token-introspects { "project_id": "{{project_gid}}", "name": "auth0-person-introspect", "display_name": "Auth0 Person Token Introspect", "description": "Introspect Auth0 tokens and upsert Person nodes", "jwt_matcher": { "issuer": "https://1st.eu.auth0.com", "audience": "N6VibJHjxIpKEvkBKSWU1xLxh6xnca55" }, "ikg_node_type": "Person", "claims_mapping": { "email": { "selector": "email" } }, "perform_upsert": true } Three things to notice: - `jwt_matcher` declares which `issuer` + `audience` token to accept. The values shown here point to an Auth0 tenant IndyKite stood up specifically to exercise the platform - a test IdP whose user accounts back the 12 `auth0|...` Person nodes you ingest in Chapter 5. Keep them as-is to follow this tutorial against that test IdP, or replace them with your own Auth0 tenant's `issuer` and `audience` to wire up your own users. - `ikg_node_type: "Person"` tells the platform that the token's `sub` claim resolves to a `Person` node. This is the binding our CIQ policies rely on via `subject.external_id = $token.sub`. - `perform_upsert: true` means a `Person` node is auto-created the first time an unknown token sub is seen - useful in development. At execute time the Person-subject policies in Chapter 8 enforce `subject.external_id = $token.sub`, where `$token.sub` resolves to the verified `sub` claim of the bearer token. No `subject_external_id` input param is sent by the client - the platform reads the claim from the bearer that Token Introspect just validated, and the policy filter pins the cypher subject to the matching `Person` node. ### What comes next Chapter 4 adds an MCP Server configuration so AI agents can call IndyKite over the Model Context Protocol. ## Chapter 4: Configure the MCP Server Stand up an MCP Server configuration so AI agents can initialize sessions and execute IndyKite tools over HTTP. ## Chapter 4: Configure the MCP Server (optional) **Optional - skip to Chapter 5 if you don't need MCP yet.** This chapter is only required if you want an AI agent to drive IndyKite over MCP. Every later chapter (ingest, KBAC/AuthZEN, ContX IQ) works fine without it - you can come back and add MCP later by running just the requests below. From the MCP guide: the IndyKite MCP Server exposes IndyKite capabilities (Capture, ContX IQ, AuthZEN, Token Introspect) to **Model Context Protocol** clients. AI agents that speak MCP can initialize a session, list available tools, and execute them - all against your project's IKG. ### Why include this in the tutorial - It's the bridge between the music graph you're about to build and any LLM-driven client (Claude, GPT, custom agents) that wants to query or update it. - The configuration call is short and depends only on values you've already captured (`project_gid`, `app_agent_gid`, `token_introspect_gid`). ### Run these requests **Request** **Endpoint** **Purpose** `create mcp server config``POST /configs/v1/mcp-servers`Captures `mcp_server_gid`. `read mcp server config``GET /configs/v1/mcp-servers/{{mcp_server_gid}}`Optional - inspect the active config. `delete mcp server config``DELETE /configs/v1/mcp-servers/{{mcp_server_gid}}`Optional - tear it down. ### Body shape The body wires the MCP Server to the agent identity that will execute tools (`app_agent_id`) and to the Token Introspect config that resolves end-user tokens (`token_introspect_id`). Scopes and an enabled flag complete the configuration. { "app_agent_id": "{{app_agent_gid}}", "token_introspect_id": "{{token_introspect_gid}}", "project_id": "{{project_gid}}", "name": "mcp-server-test", "display_name": "MCP Server test", "description": "MCP Server configuration description", "enabled": true, "scopes_supported": ["email"] } ### What comes next Chapter 5 ingests the music dataset itself - 16 thousand+ nodes and 31 thousand+ relationships - via two Capture API calls. ## Chapter 5: Ingest the music graph: nodes and relationships Use the Capture API to load the entire music dataset - Artists, Albums, Tracks, Playlists, Persons, Venues, and 13 relationship types - in two requests. ## Chapter 5: Ingest the music graph From the Environment guide: the IKG is the foundation of every IndyKite product. Authorization decisions, ContX IQ queries, and contextual lookups all run against data captured here. The collection ships the music dataset inline so you can populate the graph in two requests. ### Auth changes here Capture is data, not configuration. From this point on, requests use the **Application Agent** credential as `X-IK-ClientKey: {{your_agent_token}}` instead of the ServiceAccount Bearer. ### Run these two requests **Request** **Endpoint** **Payload size** `upsert nodes``POST /capture/v1/nodes/`15k+ nodes across 6 types `upsert relationships``POST /capture/v1/relationships`31k+ relationships across 13 types ### What gets created Nodes by type: - Artist: 94, Album: 1 247, Track: 14k+ - Playlist: 20, Person: 86, Venue: 24 Relationships by type: - Catalog: `CREATED` (14k+), `PART_OF` (14k+), `RELEASED` (1k+) - Engagement: `LIKES` (111), `FOLLOWS` (42), `SUBSCRIBED_TO` (30) - Family: `MARRIED_TO` (15), `PARENT_OF` (55), `PARTNERS` (7), `CO_PARENTS` (4) - Venue: `WILL_ATTEND` (86), `APPROVED_FOR` (318), `PLAYED_AT` (314) ### Body shape - upsert nodes `POST /capture/v1/nodes/` with header `X-IK-ClientKey: {{your_agent_token}}`. Each entry has an `external_id`, a `type` (label), and a list of typed properties. Sample slice (the full payload contains 15k+ nodes): { "nodes": [ { "external_id": "artist-2", "type": "Artist", "properties": [ { "type": "name", "value": "ABBA" } ] }, { "external_id": "venue-1", "type": "Venue", "properties": [ { "type": "name", "value": "Shower-Concert-Hall" }, { "type": "min_confidence", "value": 0.5 }, { "type": "max_loudness", "value": -3.0 } ] }, { "external_id": "auth0|69c3ee8cb9ed562744ff9326", "type": "Person", "properties": [ { "type": "firstname", "value": "Cornelius" }, { "type": "city", "value": "Ctrl-Z Canyon" }, { "type": "music_mood", "value": "Acoustic Sadness" }, { "type": "karaoke_confidence", "value": 0.4 }, { "type": "dance_skill", "value": 0.67 } ] } ] } ### Body shape - upsert relationships `POST /capture/v1/relationships` with the same `X-IK-ClientKey` header. Each entry names a `source`, a `target`, and a `type`. The platform is idempotent - running the same payload twice does not create duplicates. Sample slice (the full payload contains 31k+ relationships): { "relationships": [ { "source": { "type": "Artist", "external_id": "artist-1" }, "target": { "type": "Track", "external_id": "track-1" }, "type": "CREATED" }, { "source": { "type": "Person", "external_id": "person-52" }, "target": { "type": "Track", "external_id": "track-9381" }, "type": "LIKES" }, { "source": { "type": "Person", "external_id": "auth0|69c3ee8cb9ed562744ff9326" }, "target": { "type": "Venue", "external_id": "venue-1" }, "type": "WILL_ATTEND" } ] } ### Recommended test users The 86 Person nodes use two id schemes: 74 synthetic users with `person-N` external_ids (numbered 1-85 with a few gaps), and 12 real `auth0|...` subjects for users meant to drive end-to-end CIQ executes with a Bearer access token. The dataset is seeded so the two recommended auth0 users below cover every Person-subject query that matters: **Auth0 sub** **Person** **Edges** **Use to test** `auth0|69c3ee8cb9ed562744ff9326` Cornelius (age 14) 5 LIKES, 2 FOLLOWS, 2 SUBSCRIBED_TO, 5 WILL_ATTEND, 1 CREATED Playlist, plus PARENT_OF coming in All "own data" queries (kq4-kq9), social/taste queries (kq14-kq20), venue queries (kq12, kq13, kq22), engagement (kq24). `auth0|69c4129b0ba356c7db9f91ab` Marmaduke (parent of person-15, person-16) PARENT_OF -> person-15 / person-16; each child has 3 LIKES. Parent-side queries: kq11 (children-likes), kq10 (family-playlists). Pick the auth0 sub that matches the query you're testing - the bearer's `sub` claim is what the policy filter pins on (see Chapter 8). The other 10 auth0 users only have family relationships and will return empty for music-engagement queries. ### Verifying the ingest A non-error 200 response on each request is sufficient for now. Chapters 6-10 will exercise the data via authorization and CIQ executes. If you need to ingest a single subset or update a property later, see the Capture resource examples. ### What comes next Chapter 6 introduces KBAC policies - the yes/no authorization rules - using ten concrete music-app scenarios. ## Chapter 6: KBAC policies: define what's allowed Author ten KBAC policies covering venue performance, entry, flash mobs, track loudness gates, playlist sharing, family playlists, and DJ rights. ## Chapter 6: KBAC policies From the Dynamic Authorization guide and AuthZEN guide: **Knowledge-Based Access Control (KBAC)** is the rule layer. Each policy declares a `subject`, an `action`, a `resource` shape (via the cypher MATCH), and the `condition` the subject must satisfy. Chapter 7 turns these into yes/no decisions via the AuthZEN `/access/v1/evaluation` endpoint. ### The ten policies in the collection **Request** **Policy name** **Subject** **Action** **Resource** **Allows when…** `kbac`concert-performerPersonPERFORMVenuesubject is going to the venue and karaoke_confidence ≥ venue.min_confidence `kbac2`venue-entry-by-membershipPersonENTERVenuesubject is going to the venue (any WILL_ATTEND) `kbac3`flash-mob-recruitmentPersonJOINVenue (Flash-Mob-Recruitment)subject's dance_skill ≥ venue.min_danceability `kbac4`track-loudness-checkTrackPLAYVenuetrack is PLAYED_AT and loudness ≥ max_loudness `kbac5`dmv-endless-sufferingTrackPLAYVenue (DMV-Waiting-Area)track.energy ≤ venue.max_energy `kbac6`playlist-venue-approvalPlaylistFEATUREVenueplaylist is APPROVED_FOR the venue `kbac7`share-subscribed-playlistPersonSHAREPlaylistsubject SUBSCRIBED_TO or CREATED the playlist `kbac8`family-playlist-accessPersonVIEWPlaylista family member (MARRIED_TO / PARENT_OF / PARTNERS) CREATED the playlist `kbac9`corporate-no-enthusiasmTrackPLAYVenue (Corporate-Meeting-Room)track.energy ≤ venue.max_energy `kbac10`dj-at-venuePersonDJVenuesubject WILL_ATTEND and karaoke_confidence ≥ 0.8 ### Anatomy of one KBAC policy Each `kbacN` request posts to `POST /configs/v1/authorization-policies` with a stringified policy body. Stripped of envelope, the shape is: { "meta": { "policy_version": "1.0" }, "subject": { "type": "Person" }, "actions": ["PERFORM"], "condition": { "cypher": "MATCH (subject:Person)-[:WILL_ATTEND]->(resource:Venue) WHERE subject.property.karaoke_confidence >= resource.property.min_confidence" } } Three things to notice: - The cypher binds **two named nodes**: `subject` (matches the subject sent in the AuthZEN call) and `resource` (matches the resource sent in the AuthZEN call). - The **resource type** is implied by the cypher (`resource:Venue`), so the AuthZEN evaluation must send a Venue id. - The **action** array gates which AuthZEN calls this policy can answer (`PERFORM` here). ### Three more concrete examples **`kbac4` - Track-loudness gate (Track subject):** POST /configs/v1/authorization-policies { "project_id": "{{project_gid}}", "name": "track-loudness-check", "display_name": "Track Loudness Venue Compatibility", "policy": "{ \"meta\": {\"policy_version\":\"2.0-kbac\"}, \"subject\": {\"type\":\"Track\"}, \"actions\": [\"PLAY\"], \"resource\": {\"type\":\"Venue\"}, \"condition\": {\"cypher\":\"MATCH (subject:Track)-[:PLAYED_AT]->(resource:Venue) WHERE subject.property.loudness >= resource.property.max_loudness\"} }", "status": "ACTIVE" } **`kbac6` - Playlist venue approval (Playlist subject, no WHERE):** policy: { "subject": {"type":"Playlist"}, "actions": ["FEATURE"], "resource": {"type":"Venue"}, "condition": {"cypher":"MATCH (subject:Playlist)-[:APPROVED_FOR]->(resource:Venue)"} } **`kbac8` - Family playlist access (multi-relationship-type pattern):** policy: { "subject": {"type":"Person"}, "actions": ["VIEW"], "resource": {"type":"Playlist"}, "condition": {"cypher":"MATCH (subject:Person)-[:MARRIED_TO|PARENT_OF|PARTNERS]-(family:Person)-[:CREATED]->(resource:Playlist)"} } ### Auth All ten requests use `{{your_sa_token}}` as `Authorization: Bearer ...` (Config API). The capture script writes the returned policy GID into `policy_gid`, which is overwritten each time a new policy is created. ### What comes next Chapter 7 evaluates these policies with single AuthZEN calls and a boxcar batch. ## Chapter 7: AuthZEN evaluations: yes/no decisions on the music graph Run ten single AuthZEN evaluations and one boxcar batch against the KBAC policies authored in Chapter 6. ## Chapter 7: AuthZEN evaluations From the AuthZEN guide: AuthZEN is a standard request shape - *subject*, *resource*, *action* - that returns a yes/no decision. IndyKite implements the standard at `POST /access/v1/evaluation` for a single decision and `POST /access/v1/evaluations` for a batch (boxcar). ### Auth changes back to the agent token Evaluations are runtime data, not configuration, so they use the Application Agent credential as `X-IK-ClientKey: {{your_agent_token}}` - same as the Capture and CIQ requests. No ServiceAccount Bearer here. ### The ten single evaluations **Request** **Subject** **Resource** **Action** **Targets policy** `Evaluation`Person/`person-52`Venue/`venue-1`PERFORMkbac (concert-performer) `Evaluation2`Person/`person-52`Venue/`venue-10`ENTERkbac2 (venue-entry-by-membership) `Evaluation3`Person/`person-47`Venue/`venue-16`JOINkbac3 (flash-mob-recruitment) `Evaluation4`Track/`track-2724`Venue/`venue-1`PLAYkbac4 (track-loudness-check) `Evaluation5`Track/`track-7431`Venue/`venue-4`PLAYkbac5 (dmv-endless-suffering) `Evaluation6`Playlist/`playlist-1`Venue/`venue-1`FEATUREkbac6 (playlist-venue-approval) `Evaluation7`Person/`person-44`Playlist/`playlist-17`SHAREkbac7 (share-subscribed-playlist) `Evaluation8`Person/`person-1`Playlist/`playlist-13`VIEWkbac8 (family-playlist-access) `Evaluation9`Track/`track-7431`Venue/`venue-14`PLAYkbac9 (corporate-no-enthusiasm) `Evaluation10`Person/`person-40`Venue/`venue-3`DJkbac10 (dj-at-venue) ### Single AuthZEN body shape { "subject": { "type": "Person", "id": "person-52" }, "resource": { "type": "Venue", "id": "venue-1" }, "action": { "name": "PERFORM" } } The platform finds a policy whose `subject.type`, cypher `resource:`, and declared action all match, then evaluates the cypher condition against the IKG. The response is an AuthZEN decision (`decision: true|false`) plus optional context. ### Boxcar batch From the batch authorization resource: when a single caller needs many decisions in one round-trip, send them all to `POST /access/v1/evaluations` (note the plural). The collection's `Evaluations` request batches three sub-evaluations under a default subject of `person-52`, with one sub-evaluation overriding the subject to `person-40`: { "subject": { "type": "Person", "id": "person-52" }, "evaluations": [ { "resource": { "type": "Venue", "id": "venue-1" }, "action": { "name": "PERFORM" } }, { "resource": { "type": "Venue", "id": "venue-1" }, "action": { "name": "ENTER" } }, { "subject": { "type": "Person", "id": "person-40" }, "resource": { "type": "Venue", "id": "venue-1" }, "action": { "name": "PERFORM" } } ] } ### More single examples **`Evaluation4` - Track subject (kbac4 track-loudness):** POST /access/v1/evaluation { "subject": { "type": "Track", "id": "track-2724" }, "resource": { "type": "Venue", "id": "venue-1" }, "action": { "name": "PLAY" } } **`Evaluation6` - Playlist subject (kbac6 playlist-venue-approval):** { "subject": { "type": "Playlist", "id": "playlist-1" }, "resource": { "type": "Venue", "id": "venue-1" }, "action": { "name": "FEATURE" } } **`Evaluation8` - Person subject + Playlist resource (kbac8 family-playlist):** { "subject": { "type": "Person", "id": "person-1" }, "resource": { "type": "Playlist", "id": "playlist-13" }, "action": { "name": "VIEW" } } ### Response shape Successful AuthZEN responses are minimal: a boolean decision and optional context. For example: { "decision": true, "context": { "reason": null } } For the boxcar endpoint, the response is an array of `{ decision, context }` objects in the same order as the request's `evaluations` array. ### What comes next Chapter 8 switches to ContX IQ - data queries with policy-scoped reads and writes - by authoring 24 CIQ policies. ## Chapter 8: ContX IQ policies: context-aware data access Author 24 CIQ policies that gate what reads, writes, and deletes are allowed against the music graph - with subject scoping for Person and Application identities. ## Chapter 8: ContX IQ policies From the ContX IQ guide: ContX IQ moves authorization from "yes/no" (KBAC) to "what data can this caller actually see / change". A CIQ policy declares a `subject` type, a cypher `condition` with optional `$param` filters, and an `allowed_reads` / `allowed_upserts` / `allowed_deletes` projection over the matched graph. ### Subjects in this collection **Subject type****Used by****Auth at execute time** `_Application`System-level catalog ops (venue playable tracks, artist catalog, karaoke-ready people, attendance stats).`X-IK-ClientKey: {{your_agent_token}}` only. `Person`User-scoped reads/writes (own profile, liked tracks, playlists, family, similar taste, etc.).`X-IK-ClientKey: {{your_agent_token}}` + `Authorization: Bearer {{your_user_token}}`. ### Person-subject filter convention Every Person-subject policy in this collection includes: { "operator": "=", "attribute": "subject.external_id", "value": "$token.sub" } Without this clause, `MATCH (subject:Person)` matches every Person in the graph - not just the caller. Anchoring it to `$token.sub` means the platform plugs in the verified `sub` claim from the bearer token at execute time. There is no client-supplied `subject_external_id` input param, so callers cannot point the query at someone else's data; the bearer is the only identity source. ### The 24 policies (grouped by domain) **Group****Subject****Policies (request → name)** Application catalog ops`_Application``ciqpolicy` (venue-playable-tracks), `ciqpolicy2` (artist-catalog), `ciqpolicy3` (karaoke-ready-people), `ciqpolicy21` (venue-attendance-stats) Person - own data`Person``ciqpolicy4` (own-profile), `5` (own-liked-tracks), `6` (own-playlists), `7` (own-subscriptions), `8` (own-followed-artists), `9` (own-venues), `24` (own-playlist-engagement) Person - family`Person``ciqpolicy10` (family-playlists), `11` (children-likes) Person - venue context`Person``ciqpolicy12` (venue-other-attendees), `13` (venue-playlists), `17` (matching-venues), `18` (liked-tracks-at-venue), `22` (people-in-venue-list), `23` (eligible-venues-broad) Person - social`Person``ciqpolicy14` (same-taste), `15` (same-artist-followers), `16` (created-playlist-subscribers), `19` (popular-tracks-by-followed), `20` (liked-tracks-avg-energy) ### Anatomy of one CIQ policy Stripped of envelope, a Person-subject policy with a `$param` filter looks like: { "meta": { "policy_version": "1.0-ciq" }, "subject": { "type": "Person" }, "condition": { "cypher": "MATCH (subject:Person)-[:WILL_ATTEND]->(venue:Venue)<-[:WILL_ATTEND]-(other:Person)", "filter": [{ "operator": "AND", "operands": [ { "operator": "=", "attribute": "subject.external_id", "value": "$token.sub" }, { "operator": "=", "attribute": "venue.property.name", "value": "$venue_name" } ] }] }, "allowed_reads": { "nodes": ["other.property.firstname", "other.property.city", "venue.property.name"] } } Each `$param` in the filter becomes a required `input_params` key when the query that uses this policy is executed (Chapter 10). Missing one returns `HTTP 422 invalid_argument: missing or wrong input params`. ### Two real examples from the collection **`ciqpolicy4` - simplest Person-subject policy (read/update own profile):** POST /configs/v1/authorization-policies { "project_id": "{{project_gid}}", "name": "ciq-person-own-profile", "display_name": "Person: Read and Update Own Profile", "policy": "{ \"meta\": {\"policy_version\":\"1.0-ciq\"}, \"subject\": {\"type\":\"Person\"}, \"condition\": { \"cypher\": \"MATCH (subject:Person)\", \"filter\": [{\"operator\":\"=\",\"attribute\":\"subject.external_id\",\"value\":\"$token.sub\"}] }, \"allowed_reads\": { \"nodes\": [ \"subject.property.firstname\", \"subject.property.lastname\", \"subject.property.email\", \"subject.property.age\", \"subject.property.city\", \"subject.property.state\", \"subject.property.music_mood\",\"subject.property.karaoke_confidence\", \"subject.property.dance_skill\",\"subject.property.profession\" ] }, \"allowed_upserts\": { \"nodes\": { \"existing_nodes\": [\"subject\"] } } }", "status": "ACTIVE" } **`ciqpolicy14` - same-taste with COUNT aggregates and an OR clause:** policy: { "subject": {"type":"Person"}, "condition": { "cypher": "MATCH (subject:Person)-[:LIKES]->(track:Track) other WITH subject, other, COUNT(DISTINCT track) AS sharedTracks OPTIONAL MATCH (subject)-[:FOLLOWS]->(artist:Artist)=","value":"$min_shared_tracks"}, {"operator":"OR","operands":[ {"attribute":"other.property.city","operator":"=","value":"subject.property.city"}, {"attribute":"sharedArtists", "operator":">=","value":"$min_shared_artists"} ]} ]}] }, "allowed_reads": { "nodes": ["other.property.firstname","other.property.lastname", "other.property.city", "other.property.music_mood"], "aggregate_values": ["sharedTracks", "sharedArtists"] } } The aggregate variables produced by `WITH` become first-class `aggregate_values` the query can read - more on that in Chapter 9. ### Auth All `ciqpolicyN` requests use the ServiceAccount Bearer (`{{your_sa_token}}`) - they are Config API calls. The capture script writes the response id into `policy_gid`, which is then consumed by the Knowledge Query immediately following. ### What comes next Chapter 9 turns each policy into one or more Knowledge Queries that declare what to read or write. ## Chapter 9: Knowledge Queries: declare what to read or write Author 44 Knowledge Queries against the 24 CIQ policies - read variants, write variants, and delete variants for the music graph. ## Chapter 9: Knowledge Queries From the ContX IQ guide: a Knowledge Query (KQ) is a thin wrapper that **names a projection** over a CIQ policy. The policy controls what's *allowed*; the query says what subset of that to *actually fetch or write*. ### Why one policy can have multiple queries For a single policy like `ciq-app-venue-playable-tracks` the collection ships three variants: - `kq` - read tracks at the venue. - `kqb` - upsert (write) Track→Venue relationships allowed by the same policy. - `kqc` - delete those relationships. All three reference the same `policy_id` (`{{policy_gid}}`, the most recent CIQ policy created), but their `query` JSON expresses different node/relationship subsets. ### Query body shape { "project_id": "{{project_gid}}", "name": "kq-app-venue-playable-tracks-read", "display_name": "App Query: Read Venue's Playable Tracks", "description": "Read tracks that can be played at a venue", "policy_id": "{{policy_gid}}", "query": "{\"nodes\":[\"track.property.title\",\"track.property.loudness\",...],\"relationships\":[\"r\"]}", "status": "ACTIVE" } The `query` field is a stringified JSON. Inside, `nodes` lists the node properties to project, and `relationships` lists relationship variables (named in the policy's cypher) to include. Aggregations are referenced via `aggregate_values` when the policy declares them. ### Mapping policy → queries in the collection **Policy****Queries** `ciqpolicy``kq` (read), `kqb` (write), `kqc` (delete) `ciqpolicy2``kq2`, `kq2b`, `kq2c`, `kq2d` `ciqpolicy3``kq3` `ciqpolicy4``kq4` (read), `kq4b` (write) `ciqpolicy5``kq5`, `kq5b`, `kq5c` `ciqpolicy6``kq6`, `kq6b`, `kq6c`, `kq6d` `ciqpolicy7` - `ciqpolicy9`3 variants each (read/write/delete) `ciqpolicy10` - `ciqpolicy11`1 read each (family / children's likes) `ciqpolicy12``kq12` (read venue attendees) `ciqpolicy13`3 variants (read/write/delete venue playlists) `ciqpolicy14` - `ciqpolicy16`1 read each (social / matching) `ciqpolicy17`2 (read, write) `ciqpolicy18` - `ciqpolicy24`1 query each (analytics & engagement) Total: **44 Knowledge Queries** across the 24 policies. Run them in document order so the capture script can pair each one to its policy. ### Run-order rule The collection is laid out so that every `ciqpolicyN` creation is immediately followed by its query (or queries). The capture script overwrites `policy_gid` on every policy create and `query_gid` on every query create. Each `ciq exec ...` request in the next chapter uses the most recent `query_gid`, so a policy → query → execute triplet always sees consistent values. ### Real KQ bodies from the collection **`kq4` - read own profile (paired with ciqpolicy4):** POST /configs/v1/knowledge-queries { "project_id": "{{project_gid}}", "name": "kq-person-own-profile-read", "display_name": "Person Query: Read Own Profile", "policy_id": "{{policy_gid}}", "query": "{ \"nodes\": [ \"subject.property.firstname\", \"subject.property.lastname\", \"subject.property.email\", \"subject.property.age\", \"subject.property.city\", \"subject.property.music_mood\" ], \"relationships\": [] }", "status": "ACTIVE" } **`kq14` - same-taste with aggregate values (paired with ciqpolicy14):** { "name": "kq-person-same-taste", "policy_id": "{{policy_gid}}", "query": "{ \"nodes\": [ \"other.property.firstname\", \"other.property.lastname\", \"other.property.city\", \"other.property.music_mood\" ], \"aggregate_values\": [\"sharedTracks\", \"sharedArtists\"] }", "status": "ACTIVE" } **`kq22` - venue whitelist (uses `venue.property.name` projection):** { "name": "kq-person-people-in-venue-list", "policy_id": "{{policy_gid}}", "query": "{ \"nodes\": [ \"other.property.firstname\", \"other.property.city\", \"venue.property.name\" ] }", "status": "ACTIVE" } ### Auth All KQ creation requests use the ServiceAccount Bearer (`{{your_sa_token}}`) - same as the policies they reference. ### What comes next Chapter 10 actually executes each Knowledge Query against the music graph using values drawn from the seeded dataset. ## Chapter 10: Execute the Knowledge Queries against the music graph Run all 44 paired ciq exec requests, supplying the right input_params and auth headers so each query returns real music-dataset rows. ## Chapter 10: Execute the Knowledge Queries From the ContX IQ guide: a CIQ execute is what actually fetches data. It posts to `POST /contx-iq/v1/execute` with the query's `id`, the values for each `$param` declared by the underlying policy, and (for Person subjects) the user's bearer token. ### One execute per query For each `kq...` in Chapter 9 the collection ships a paired `ciq exec ...` request positioned immediately after it. Naming convention: `ciq exec kq`, `ciq exec kqb`, ..., `ciq exec kq24`. Total: **44 executes**. ### Auth differs by subject type **Subject****Headers** `_Application` (9 executes)`X-IK-ClientKey: {{your_agent_token}}` only. `Person` (35 executes)`X-IK-ClientKey: {{your_agent_token}}` **and** `Authorization: Bearer {{your_user_token}}`. Set `your_user_token` in the Variables tab to a real end-user access token from your IdP before running any Person-subject execute. The token's `sub` claim is what Token Introspect (Chapter 3) uses to resolve the caller to a Person node. **The collection is optimized for one end user: Millicent Contextsworth.** The Person-subject executes are written to be run as a single caller, and the seeded `input_params` assume that caller is the Person node `Millicent Contextsworth` - `external_id: auth0|69c410bfb501d3a02bd01ea2`. So set `your_user_token` to a token *for Millicent*. The Person executes are ordered write-before-read, so running them top to bottom as Millicent first creates the referenced data (likes a track, creates/subscribes a playlist, follows an artist, attends a venue) and then the matching read/update/delete executes return it - all resolved to her via `$token.sub`. **Mint Millicent's user token.** Use IndyKite's toolbelt.indykite.com to issue a Person-subject user token: sign in with Millicent's **email** `millicent@contextsworth.com` and the shared **access code** below. The minted token's `sub` resolves to her Person node (`external_id: auth0|69c410bfb501d3a02bd01ea2`). The toolbelt issues tokens against the same test Auth0 tenant referenced by Chapter 3's `jwt_matcher`. All seeded Auth0 test users share one access code (intentionally public, rotated periodically): Reveal code``. To run as a different seeded user instead, sign in with that user's email and the same code; the bundled example values, though, are tuned for Millicent. The **Music app** - the collection plus all the data needed to recreate the whole environment (nodes, relationships, policies, queries, executes) - can be cloned from github.com/indykite/developer-hub/music. If you use your own IdP instead, you'll need a matching Token Introspect configuration (Chapter 3) pointing at that IdP's `issuer` + `audience`; then mint a token for one of your own users and upsert a matching `Person` node via the Capture API (with `external_id` equal to the token's `sub`) so the CIQ executes resolve to it. ### Execute body shape After the policy tightening in Chapter 8 (`subject.external_id = $token.sub`), the only values left in `input_params` are the policy's own `$param` filters. Identity comes from the bearer token, not the body. { "id": "{{query_gid}}", "input_params": { "venue_name": "Shower-Concert-Hall" }, "page_token": 1 } ### Sample defaults wired into the collection **Param****Default value****Why this works** Person identity (via Bearer)`{{your_user_token}}` for an Auth0 user whose `sub` matches a seeded Person `external_id` (e.g. Cornelius `auth0|69c3ee8cb9ed562744ff9326`).The bearer's verified `sub` claim drives `$token.sub` in the policy filter - no body field needed. `venue_name``Shower-Concert-Hall`First Venue in the dataset (`venue-1`); has 50 PLAYED_AT tracks and 3 attendees. `artist_name``ABBA`Seeded as `artist-2`; has 9 albums and 112 tracks. `venue_ids``["venue-1","venue-2"]`Existing Venue external_ids. `min_confidence` / `min_likes` / `min_shared_*``0.7` / `1`Permissive thresholds that return rows on this dataset. ### Real execute bodies from the collection **Smallest possible Person execute** (`ciq exec kq4`, own-profile - the policy has no `$param` beyond identity, and identity is supplied by the bearer, so `input_params` is empty): POST /contx-iq/v1/execute Headers: X-IK-ClientKey: {{your_agent_token}} Authorization: Bearer {{your_user_token}} { "id": "{{query_gid}}", "input_params": {}, "page_token": 1 } **Person execute with policy params** (`ciq exec kq14`, same-taste): { "id": "{{query_gid}}", "input_params": { "min_shared_tracks": 1, "min_shared_artists": 1 }, "page_token": 1 } **Application-subject execute** (`ciq exec kq`, venue playable tracks - no Bearer header, only the agent key): POST /contx-iq/v1/execute Headers: X-IK-ClientKey: {{your_agent_token}} { "id": "{{query_gid}}", "input_params": { "venue_name": "Shower-Concert-Hall" }, "page_token": 1 } ### Sample response (read execute) A successful read execute returns rows in the projected shape declared by the query, plus pagination metadata. Truncated example for `ciq exec kq4` (read own profile) when authenticated as Cornelius: { "rows": [ { "subject": { "property": { "firstname": "Cornelius", "lastname": "Pickleworth", "email": "cornelius@pickleworth.com", "age": 14, "city": "Ctrl-Z Canyon", "music_mood": "Acoustic Sadness", "karaoke_confidence": 0.4, "dance_skill": 0.67 } } } ], "next_page_token": null } ### What happens at execute time - The platform looks up the query by `id` and resolves its policy. - It validates that every `$param` from the policy's filter is present in `input_params` - missing one returns `HTTP 422`. - For Person subjects, it introspects the bearer token, extracts the `sub` claim, and uses it as the value of `$token.sub` in the policy filter (which then pins the cypher subject to that one Person node). - It runs the cypher with the params plugged in, then projects only the fields listed in `allowed_reads`. - The result is the policy-scoped subset of the music graph matching the query's projection. ### What to expect when you run them With the seeded dataset and the recommended test users from Chapter 5, every paired execute returns rows. Predicted row counts (auth as **Cornelius** unless noted): **Group****Executes****Rows** Application catalog (no bearer)`ciq exec kq` / `kqb` / `kqc`50 tracks at Shower-Concert-Hall `ciq exec kq2` / `kq2b` / `kq2c` / `kq2d`9 albums + 112 tracks for ABBA `ciq exec kq3`3 karaoke-ready persons `ciq exec kq21`3 attendees of Shower-Concert-Hall Person, self-only`kq4` / `kq4b` / `kq17` / `kq17b` / `kq23`1 row (the caller's own profile / properties) Person, direct edges`kq5`/b/c (likes), `kq6`/b/c/d (playlists), `kq7`/b/c (subscriptions), `kq8`/b/c (followed artists), `kq9`/b/c (venues), `kq14-kq16`, `kq18-kq20`, `kq24`1 - 5 rows each (Cornelius's seeded engagement) Two-hop (venue / family)`kq10` (family playlists), `kq12`, `kq13`/b/c, `kq22`ROWS Parent-side query`kq11` (children-likes)EMPTY for Cornelius (he's a child); switch the bearer to **Marmaduke** (`auth0|69c4129b0ba356c7db9f91ab`) and it returns 6 rows. If a Person execute returns zero rows, check (a) the bearer's `sub` claim matches a seeded Person external_id and (b) that user actually has the relationship the cypher walks. Most often the answer is "use Cornelius for music engagement, Marmaduke for parent-side queries" - see the Chapter 5 test-user table. ### Recommended order Run each `ciqpolicyN → kqN... → ciq exec ...` triplet sequentially so `policy_gid` and `query_gid` stay in sync. The collection is already sorted that way - just hit "Run" and watch the variables rotate. ### You're done By the end of this chapter you have run every product surface IndyKite exposes against a non-trivial music graph: environment provisioning, capture, KBAC + AuthZEN, Token Introspect, MCP, ContX IQ policies + queries + executes. Re-use these patterns against your own data: keep the variable layout, swap the dataset, adjust the policies. ### Where to go next - Trust Score - assess data freshness, origin, and verification. - External Data Resolver - pull in external API data during a query. - Outbound Events - stream every IKG change to Kafka, Event Grid, Service Bus, or Pub/Sub. --- Source: https://developer.indykite.com/tutorials/tutorial-music-dataset --- --- name: indykite-agent-gateway description: Deploy and configure IndyKite Agent Gateway (IAG) in front of agent-to-agent (A2A) workflows or MCP servers. Use when wiring up A2A or MCP policy enforcement, modeling workflows in the IKG, or debugging IAG 401/403 responses. license: Apache-2.0 compatibility: Requires Docker and Docker Compose for the iag-demo reference deployment, or a Kubernetes cluster for production. Runtime network access to the configured IndyKite Hub, OAuth IdP, AuthZEN, and ContX IQ endpoints is required. --- # IndyKite Agent Gateway The Indykite Agent Gateway (IAG) is a standalone service that protects exactly one downstream - an **A2A agent** or an **MCP server**. Deploy one IAG per protected downstream. From the caller's perspective IAG appears as the Target; from the protected downstream's perspective IAG appears as the Source. IAG is **not a generic reverse proxy**: by default (`protocol: a2a`) it speaks the **A2A protocol** and tracks A2A sessions so JSON-RPC streams flow correctly; with `protocol: mcp` it proxies **MCP Streamable HTTP** traffic (forwarding `Mcp-Session-Id` and streaming SSE responses through). Either way the same authorization runs in front. For each request IAG validates three things: 1. The **caller** (token introspection at the IdP). 2. The **workflow** (subject `CAN_TRIGGER` check via AuthZEN/KBAC). 3. The **delegation chain** (the request's `act` chain matches a chain modeled in the IKG). ## When to use Activate this skill when the user: - is deploying an agent-to-agent (A2A) workflow and wants policy enforcement in front of each agent; - is putting an enforcement point in front of an **MCP server** (`protocol: mcp`) so MCP traffic gets the same introspection, AuthZEN check, and audit as A2A; - needs traceable user-to-agent delegation through OAuth token exchange and the `act` chain; - is modeling a `Workflow` and `Agent` nodes with `INVOKES` relationships in the IndyKite Graph (IKG); - is configuring `JARVIS_*` environment variables or a `config.yaml` for one or more IAG instances; - is reading IAG audit records (`AUTHORIZED` / `NOT_AUTHORIZED`) and trying to explain a `403`; - is reproducing or extending the [`iag-demo`](https://github.com/indykite/developer-hub/tree/master/a2a/iag-demo) (A2A) or [`iag-mcp-demo`](https://github.com/indykite/developer-hub/tree/master/a2a/iag-mcp-demo) (A2A + MCP) reference applications. Do **not** activate this skill when the user: - wants a generic HTTP reverse proxy or service mesh - IAG only speaks A2A or MCP, not arbitrary HTTP; - is asking about IndyKite features unrelated to A2A (token introspection alone, plain AuthZEN policies, ContX IQ queries outside of agent gating); - is debugging the protected agent itself rather than the gateway in front of it. ## Prerequisites Before any of the steps below will succeed, the user needs: - An **IndyKite project** with AuthZEN / KBAC and ContX IQ enabled, plus a `Workflow` node and `Agent` nodes already modeled (or a plan to model them - see Step 1). - An **OAuth2-compliant IdP** with introspect, client-credentials, and token-exchange endpoints. Set `IDP_BASE_URL` to the IdP base for the target environment. - One **client_id / client_secret pair per protected downstream** (A2A agent or MCP server), registered with the IdP. - A **ContX IQ query** that returns `(workflow, agent_list)` pairs for each protected downstream - its `query_id` goes into IAG configuration. - For an **MCP downstream** (`protocol: mcp`): the MCP server's origin and endpoint path, and the gateway image `indykite/agent-gateway` **≥ 2.0.1** (older images ignore the protocol and behave as an A2A proxy). MCP clients typically authenticate with an App Agent token, which must be introspectable and pass the AuthZEN check. - Docker (and Docker Compose) for the reference deployment, or a Kubernetes cluster for production. If any of these are missing, stop and tell the user - IAG cannot run without them. ## Steps ### 1. Model the workflow in the IKG Create one `Workflow` node identified by `external_id`, one `Agent` node per protected agent, and `INVOKES` relationships between agents. Every `INVOKES` relationship **must** carry a `workflow_name` property whose value matches the `Workflow.external_id`. The ContX IQ query in step 3 silently excludes any chain without it. Example shape (the canonical `wf1` workflow from the demo): ```text (Workflow {external_id: "wf1"}) (Agent {external_id: "orchestrator"}) -[INVOKES {workflow_name: "wf1"}]-> (Agent {external_id: "retriever"}) (Agent {external_id: "orchestrator"}) -[INVOKES {workflow_name: "wf1"}]-> (Agent {external_id: "weather"}) ``` Capture this data through the **Capture API** (`POST /capture/v1/nodes`, `POST /capture/v1/relationships`), the IndyKite Hub UI, or an existing Terraform / identity pipeline - whichever the project already uses. ### 2. Wire the subject to the workflow For every subject that is allowed to trigger the workflow, create the edge `(:User)-[:CAN_TRIGGER]->(:Workflow)` (or the equivalent AuthZEN relation). Without this edge IAG returns `403` at the AuthZEN check even if the chain is correct. ### 3. Build the ContX IQ query The query must return `(workflow, agent_list)` pairs given a protected agent identifier, where `agent_list` is the ordered chain of agents the request must traverse. Save its `query_id` - IAG references it via `JARVIS_CONTX_IQ_QUERY_ID` (or `contx_iq.query_id`). ### 4. Configure each IAG instance Pick one of the two configuration forms: - **YAML config file** - pass with `--config=/app/config.yaml`. Best for production where configuration management is strict. See `assets/config-template.yaml` in this skill. - **Environment variables** - keys use the `JARVIS_` prefix with underscores (e.g. `JARVIS_SERVICE_NAME`). Best for Docker Compose deployments where multiple IAG instances share a base image and override only per-agent fields. The full set of sections is `service`, `identity_provider`, `protected_agent`, `authzen`, `contx_iq`, and `audit`. See `references/configuration.md` for every field and its default. Pick the downstream protocol with `protected_agent.protocol` (env `JARVIS_PROTECTED_AGENT_PROTOCOL`): `a2a` (default) for an A2A agent, `mcp` for an MCP server. Any other value fails startup with *invalid protected_agent protocol*. In `mcp` mode `protected_agent.base_url` is the MCP server **origin only** - IAG appends the incoming request path. The authorization sequence is unchanged; only the forwarded protocol differs. See the *Protecting an MCP server* section of `references/configuration.md`. Per-instance values that **must** differ between IAG instances: - `service.name` / `JARVIS_SERVICE_NAME` - `service.port` / `JARVIS_SERVICE_PORT` - `protected_agent.base_url` / `JARVIS_PROTECTED_AGENT_BASE_URL` - `protected_agent.protocol` / `JARVIS_PROTECTED_AGENT_PROTOCOL` (if any instance protects an MCP server) - `protected_agent.authentication.client_id` and `client_secret` ### 5. Deploy the IAG instances For Docker Compose, follow the iag-demo pattern: one shared `iag-base-docker.yaml` plus one service per protected agent that overrides only the per-instance fields above. For Kubernetes, deploy each IAG as a standalone Pod for independent scaling, or as a sidecar to its protected agent for tighter coupling. Verify that each IAG can reach the IdP, AuthZEN, ContX IQ, and the protected agent - common deployment failures are network-level, not IAG-level. ### 6. Verify the runtime path Send a request through the gateway and confirm the [nine-step path](/agent-skills/indykite-agent-gateway/references/architecture.md) executes end-to-end. The expected HTTP responses are: - `200` - request was authorized and forwarded. - `400` - bad request. - `401` - caller token missing or inactive. - `403` - caller authenticated but not allowed (subject, chain, or both). - `500` - internal error. - `502` - upstream / gateway-side processing error. Watch service logs (JSON to stdout) and audit records together - service logs explain *what IAG did*, audit records explain *what IAG decided*. ### 7. Read the audit trail Audit records are emitted as a separate stream from service logs. Configure delivery as either: - **Webhook** - `audit.delivery: webhook`, `audit.http.url`, optional auth (`mTLS`, `api-key`, `basic`, `no-auth`). - **File** - `audit.delivery: file`, `audit.storage_path`, `audit.format` (`csv`, `json`, `txt`), `audit.rotation_strategy` (`size`, `time`, `size_and_time`). Each record contains `decision`, `reason`, `subject`, `actor`, `action`, `service`, `timestamp`, `traceID`. The `traceID` correlates with the protected agent's logs and the upstream client. ### 8. Exercise denial paths intentionally To gain confidence that IAG is enforcing as expected, deliberately force `NOT_AUTHORIZED` outcomes: - **Skip an agent in the chain** (e.g. call `retriever-iag` directly without the orchestrator in `act`). - **Remove `workflow_name`** from one `INVOKES` relationship - ContX IQ stops returning that chain. - **Delete the `CAN_TRIGGER` edge** between the subject and the workflow - AuthZEN says no. - **Use a subject whose type is not in `JARVIS_AUTHZEN_SUBJECT_TYPES`** - no policy matches. Each should return `403 Forbidden` and produce a `NOT_AUTHORIZED` audit record with a useful `reason`. ## Outcome When this skill has been applied successfully: - A `Workflow` node, the relevant `Agent` nodes, and `INVOKES` edges (with `workflow_name`) exist in the IKG. - A ContX IQ query returns the right `(workflow, agent_list)` pairs for each protected agent. - One IAG instance runs in front of each protected agent with the right per-instance config. - A canonical successful prompt flows through the gateway chain and produces `AUTHORIZED` audit records on every IAG it touches. - A canonical denial path produces `403 Forbidden` plus a `NOT_AUTHORIZED` audit record with a human-readable `reason`. ## Files in this skill - [`references/architecture.md`](/agent-skills/indykite-agent-gateway/references/architecture.md) - the nine-step IAG request path and the IKG data shape. - [`references/configuration.md`](/agent-skills/indykite-agent-gateway/references/configuration.md) - every IAG configuration section, field, and default. - [`references/troubleshooting.md`](/agent-skills/indykite-agent-gateway/references/troubleshooting.md) - common failure modes mapped to fixes. - [`assets/config-template.yaml`](/agent-skills/indykite-agent-gateway/assets/config-template.yaml) - a starter `config.yaml` to copy and adapt. ## Agent-specific notes This skill uses generic markdown instructions and works across all agents listed in the [README](/agent-skills/README.md). It does not require Claude Code hooks, Cursor `@`-mentions, Copilot workspace context, or any agent-specific feature. Network access (`curl`, the agent's web tools, or MCP) is needed only if the agent will call IndyKite APIs directly during a task; for setup-only work no special tools are required. ## References - [IndyKite Agent Gateway documentation](https://docs.indykite.com/docs/agent-gateway) - [`iag-demo` reference app](https://github.com/indykite/developer-hub/tree/master/a2a/iag-demo) - A2A only. - [`iag-mcp-demo` reference app](https://github.com/indykite/developer-hub/tree/master/a2a/iag-mcp-demo) - adds an `mcp-iag` instance (`protocol: mcp`) protecting the IndyKite MCP server. - [`canbank` dataset](https://github.com/indykite/developer-hub/tree/master/canbank) - A2A protocol - see the protected agent's vendor docs for the JSON-RPC shape IAG forwards. For MCP, IAG proxies MCP Streamable HTTP (`initialize`, `tools/list`, `tools/call`, …). --- --- name: indykite-authzen-evaluation description: Make a single KBAC authorization decision via the IndyKite AuthZEN REST API (`POST /access/v1/evaluation`) - returns a boolean `decision` for one (subject, action, resource) triple, optionally with per-request `context.input_params`. Use for a single yes/no question - "can ada PROVISION gpu-node-7?", "is this user allowed to delete this document?", "gate this operation on a live check", or debugging why one decision is false. Not for many checks at once (use indykite-authzen-evaluations), not for enumerating which actions/resources/subjects are allowed (use indykite-authzen-search-action / -search-resource / -search-subject), and not for authoring the policy behind the decision (use indykite-authzen-kbac-policies). For the same decision over MCP/JSON-RPC see indykite-mcp-server (`authzen_evaluate`). 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 AuthZEN - single authorization decision A KBAC decision asks the AuthZEN endpoint one question - *may this subject perform this action on this resource?* - and gets back a boolean `decision`. The decision is rendered by evaluating the project's currently ACTIVE `2.0-kbac` policies against the IKG. This skill covers making that **single** decision: framing the `(subject, action, resource, context)` request, sending it, and reading the boolean. It does **not** author policies - the policy whose `subject` / `actions` / `resource` / `condition.cypher` the decision is evaluated against is authored with [`indykite-authzen-kbac-policies`](/agent-skills/indykite-authzen-kbac-policies/SKILL.md). It is the single-call member of the AuthZEN family: | Need | Endpoint | Skill | |-------------------------------------------------|------------------------------|-----------------------------------------------------------------------| | **One** yes/no decision | `/access/v1/evaluation` | this skill | | Many decisions at once | `/access/v1/evaluations` | [`indykite-authzen-evaluations`](/agent-skills/indykite-authzen-evaluations/SKILL.md) | | Actions a subject may perform on a resource | `/access/v1/search/action` | [`indykite-authzen-search-action`](/agent-skills/indykite-authzen-search-action/SKILL.md) | | Resources a subject may act on, given an action | `/access/v1/search/resource` | [`indykite-authzen-search-resource`](/agent-skills/indykite-authzen-search-resource/SKILL.md) | | Subjects allowed an action on a resource | `/access/v1/search/subject` | [`indykite-authzen-search-subject`](/agent-skills/indykite-authzen-search-subject/SKILL.md) | | Author / manage the KBAC policy | Config API | [`indykite-authzen-kbac-policies`](/agent-skills/indykite-authzen-kbac-policies/SKILL.md) | ## When to use Activate this skill when the user wants to: - decide whether **one subject may perform one action on one resource** (e.g. "can `ada` `PROVISION` the server `gpu-node-7` within a budget of 120000?"); - gate an operation in an application on a live authorization check; - debug why a single decision comes back `true` or `false`. Do **not** activate this skill to **author or modify** the policy behind the decision ([`indykite-authzen-kbac-policies`](/agent-skills/indykite-authzen-kbac-policies/SKILL.md)), to make **many** decisions in one call ([`indykite-authzen-evaluations`](/agent-skills/indykite-authzen-evaluations/SKILL.md)), to **enumerate** the allowed actions/resources/subjects rather than test one triple (the search skills [`-search-action`](/agent-skills/indykite-authzen-search-action/SKILL.md) / [`-search-resource`](/agent-skills/indykite-authzen-search-resource/SKILL.md) / [`-search-subject`](/agent-skills/indykite-authzen-search-subject/SKILL.md)), or to **return or modify graph data** (a decision is yes/no, not a data read or write). ## Prerequisites - One or more **ACTIVE KBAC policies** whose `subject.type` / `actions` / `resource.type` cover the triple. If none exist, author them first with [`indykite-authzen-kbac-policies`](/agent-skills/indykite-authzen-kbac-policies/SKILL.md); a decision with no matching policy is simply `false`. - An **AppAgent** with credentials configured for the calling application ([Credentials guide](https://developer.indykite.com/guides/guide-credentials)). - The **IKG populated** with the subject and resource nodes (and any relationships the condition matches). Evaluation reads the graph; it does not seed it. - Every **partial parameter** the matched policy references, ready to pass under `context.input_params`. If a prerequisite is missing, say so - fixing it first is far cheaper than debugging an opaque `false` decision. ## Steps ### 1. Frame the triple and its context Pin the three parts of the question, plus any per-request values: | Part | Field | Example | |-----------|--------------------------------|--------------------------| | subject | `subject.type` + `subject.id` | `Person` / `ada` | | action | `action.name` | `PROVISION` | | resource | `resource.type` + `resource.id`| `Server` / `gpu-node-7` | | context | `context.input_params` | `{ "max_price": 120000 }`| `subject.id` / `resource.id` are the nodes' `external_id`s; `action.name` is case-sensitive. ### 2. Build the request body ```json { "subject": { "type": "Person", "id": "ada" }, "resource": { "type": "Server", "id": "gpu-node-7" }, "action": { "name": "PROVISION" }, "context": { "input_params": { "max_price": 120000 } } } ``` Supply `context.input_params` only when the matched policy's condition references a `$name` partial parameter; write each key **without** the leading `$`, keeping its type (numbers stay numbers). A ready body: [`assets/evaluation-provision-server.json`](/agent-skills/indykite-authzen-evaluation/assets/evaluation-provision-server.json). ### 3. Send the decision request ```text POST /access/v1/evaluation ``` The endpoint authenticates the **calling application** (its AppAgent credentials - always required) and **optionally the user** (an access token - applies only in some cases, e.g. a condition references a token claim/scope). Which credential goes in which request header is covered by the [Credentials guide](https://developer.indykite.com/guides/guide-credentials). A runnable shell helper builds the authenticated request: [`scripts/evaluate.sh`](/agent-skills/indykite-authzen-evaluation/scripts/evaluate.sh) — run with `--print` to preview the `curl` (host-pinned; tokens redacted). ### 4. Read the decision ```json { "decision": true } ``` `true` means at least one ACTIVE policy granted the `(subject, action, resource)` triple with its condition satisfied. `false` means no policy granted it - either no policy matched the triple, or the matching policy's condition did not hold for the supplied `input_params` and graph data. A `false` decision is a normal `200`, not an error. ### 5. Verify If the decision is not what you expected, walk this checklist before changing anything: 1. **Policy ACTIVE?** A draft/inactive policy is ignored. 2. **Triple matches?** `subject.type`, `action.name`, and `resource.type` must match the policy's `subject.type`, `actions`, and `resource.type` exactly (case-sensitive verbs). 3. **Variables named `subject` / `resource`?** The condition binds the request's subject/resource only through those reserved names. 4. **IDs are `external_id`s?** `subject.id` / `resource.id` are matched against node `external_id`. A wrong id silently matches nothing → `false`. 5. **Every `$param` supplied?** A missing `input_params` key the condition needs yields a `422` (`errors: ["missing or wrong input params, ''"]`), not a silent `false`. 6. **Data actually in the IKG?** Confirm the subject node, resource node, and any matched relationship exist. Full request/response schema, error table, and a deeper troubleshooting walk-through: [`references/evaluation-reference.md`](/agent-skills/indykite-authzen-evaluation/references/evaluation-reference.md) and [`references/troubleshooting.md`](/agent-skills/indykite-authzen-evaluation/references/troubleshooting.md). ## Outcome When this skill has been applied successfully: - `POST /access/v1/evaluation` returns `{"decision": true}` for an allowed `(subject, action, resource)` triple with valid `input_params`, and `{"decision": false}` for a denied one. - A missing required `input_params` key is surfaced as a `422` and corrected, not mistaken for a denial. - The same triple can be fanned out through [`indykite-authzen-evaluations`](/agent-skills/indykite-authzen-evaluations/SKILL.md) or invoked through the [`indykite-mcp-server`](/agent-skills/indykite-mcp-server/SKILL.md) `authzen_evaluate` tool with identical decision semantics. ## Files in this skill - [`references/evaluation-reference.md`](/agent-skills/indykite-authzen-evaluation/references/evaluation-reference.md) - the `/evaluation` endpoint: base path, auth, request/response shape, error codes, and pointers to the batch and search sibling skills. - [`references/troubleshooting.md`](/agent-skills/indykite-authzen-evaluation/references/troubleshooting.md) - why a decision is unexpectedly `true`/`false` and how to isolate the responsible policy. - [`assets/evaluation-provision-server.json`](/agent-skills/indykite-authzen-evaluation/assets/evaluation-provision-server.json) - runnable single-evaluation request body for the `Person PROVISION Server` example. - [`scripts/evaluate.sh`](/agent-skills/indykite-authzen-evaluation/scripts/evaluate.sh) - Bash helper that posts a decision request to `/access/v1/evaluation` (host-pinned; `--print` to preview). ## Agent-specific notes This skill uses generic markdown instructions and works across all agents listed in the [README](/agent-skills/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 - [AuthZEN guide (developer hub)](https://developer.indykite.com/guides/guide-authzen) - [Dynamic authorization with Knowledge Graphs (developer hub)](https://developer.indykite.com/guides/guide-dynamic-authz) - [KBAC: Relationship-Based Authorization with the AuthZEN API](https://developer.indykite.com/resources/authz-1) - [KBAC: Parameterized Authorization with Input Params (`max_price`)](https://developer.indykite.com/resources/authz-4) - [Music dataset tutorial (worked KBAC + AuthZEN example)](https://developer.indykite.com/tutorials/tutorial-music-dataset) - [Credentials guide](https://developer.indykite.com/guides/guide-credentials) --- --- name: indykite-authzen-evaluations description: Run many KBAC authorization decisions in one call via the IndyKite AuthZEN REST API (`POST /access/v1/evaluations`), with top-level subject/action/resource/context as defaults overridden per entry; returns one `decision` per entry, in order. Use when checking a known, fixed set of checks at once - one subject against many resources, one action across many subjects, or any mix of triples - e.g. "of these servers, which can grace provision?", "for each of these users, can they deploy gpu-node-7?". For a single check use indykite-authzen-evaluation; to enumerate ALL allowed actions/resources/subjects (open-ended, not a fixed list) use indykite-authzen-search-action / -search-resource / -search-subject; to author the policy use indykite-authzen-kbac-policies. 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 AuthZEN - batch evaluations Batch evaluation makes **many KBAC decisions in one request**. You supply top-level `subject` / `action` / `resource` / `context` as **defaults** and an `evaluations[]` array where each entry overrides only the parts it specifies; the response carries one boolean `decision` per entry, in order. This skill covers building and sending the batch request and reading the results. It does **not** author policies - the `2.0-kbac` policies every entry is evaluated against are authored with [`indykite-authzen-kbac-policies`](/agent-skills/indykite-authzen-kbac-policies/SKILL.md). ## When to use Activate this skill when the user wants to: - check **one subject against many resources** (e.g. "of these 20 servers, which can `grace` `PROVISION`?"); - check **one action across many subjects** (e.g. "which of these people can deploy `gpu-node-7`?"); - evaluate a **heterogeneous mix** of `(subject, action, resource)` triples in a single call. Do **not** activate this skill to make a **single** yes/no decision ([`indykite-authzen-evaluation`](/agent-skills/indykite-authzen-evaluation/SKILL.md)), to enumerate **all** instances for one probe (the search skills [`indykite-authzen-search-action`](/agent-skills/indykite-authzen-search-action/SKILL.md) / [`-search-resource`](/agent-skills/indykite-authzen-search-resource/SKILL.md) / [`-search-subject`](/agent-skills/indykite-authzen-search-subject/SKILL.md)), or to **author a policy** or **read/write graph data** (batch evaluation only renders decisions). ## Prerequisites - One or more **ACTIVE KBAC policies** covering the triples in the batch. If none exist, author them first with [`indykite-authzen-kbac-policies`](/agent-skills/indykite-authzen-kbac-policies/SKILL.md). - An **AppAgent** and its **credentials token** (the `X-IK-ClientKey` value). - The **IKG populated** with the subject and resource nodes (and any relationships the conditions match). - Every **partial parameter** the matched policies reference, ready to pass under `context.input_params`. ## Steps ### 1. Choose defaults, then vary per entry Decide which parts are constant across the batch (put them at the top level) and which vary (put them in each `evaluations[]` entry). An entry inherits every top-level part it does not override. > Running example: one action (`PROVISION`) and one resource (`gpu-node-7`) are the defaults; the **subject** varies per entry, and one entry also overrides the resource. ### 2. Build the batch request body ```json { "action": { "name": "PROVISION" }, "resource": { "type": "Server", "id": "gpu-node-7" }, "evaluations": [ { "subject": { "type": "Person", "id": "linus" } }, { "subject": { "type": "Person", "id": "grace" } }, { "subject": { "type": "Person", "id": "grace" }, "resource": { "type": "Server", "id": "edge-box-2" } }, { "subject": { "type": "Person", "id": "dennis" } } ], "context": { "input_params": { "max_price": 80000 } } } ``` `subject.id` / `resource.id` are node `external_id`s; `action.name` is case-sensitive; `context.input_params` keys are written **without** the `$` and keep their types. A ready body: [`assets/evaluations-provision-servers.json`](/agent-skills/indykite-authzen-evaluations/assets/evaluations-provision-servers.json). ### 3. Send the batch ```text POST /access/v1/evaluations ``` Authentication: - **Always**: `X-IK-ClientKey: `. - **Optional**: `Authorization: Bearer ` - applies only in some cases (e.g. a condition references a token claim/scope), where it can flip claim-gated entries. A runnable shell helper: [`scripts/evaluate-batch.sh`](/agent-skills/indykite-authzen-evaluations/scripts/evaluate-batch.sh) — run with `--print` to preview the `curl` (host-pinned; tokens redacted). ### 4. Read the decisions - one per entry, in order ```json { "evaluations": [ { "decision": false }, { "decision": true }, { "decision": false }, { "decision": true } ] } ``` The array is positional: `evaluations[i]` is the decision for request entry `i`. ### 5. Watch the missing-parameter difference A **single** `/evaluation` that omits a required partial parameter returns **`422`**. A **batch** call does **not** fail wholesale - it returns `200`, and each entry whose matched policy needed the missing parameter comes back as `decision: false` with a `context.reason`: ```json { "decision": false, "context": { "reason": "invalid_argument: missing or wrong input params, 'max_price'" } } ``` So always distinguish a genuine deny (`decision: false`, no `context`) from a missing-input deny (`decision: false` **with** `context.reason`) before concluding access is denied. ## Outcome When this skill has been applied successfully: - `POST /access/v1/evaluations` returns an `evaluations[]` array with one `decision` per request entry, in order, with top-level parts correctly applied as defaults. - Missing-parameter entries are read as `decision: false` + `context.reason` (a `200`), not mistaken for a request failure. - Each batch decision matches what the single-decision skill ([`indykite-authzen-evaluation`](/agent-skills/indykite-authzen-evaluation/SKILL.md)) would return for the same triple. ## Files in this skill - [`references/evaluations-reference.md`](/agent-skills/indykite-authzen-evaluations/references/evaluations-reference.md) - `/evaluations`: request/response shapes, defaults-and-override semantics, the missing-parameter behaviour, error codes. - [`assets/evaluations-provision-servers.json`](/agent-skills/indykite-authzen-evaluations/assets/evaluations-provision-servers.json) - runnable batch request body for the `PROVISION` example. - [`scripts/evaluate-batch.sh`](/agent-skills/indykite-authzen-evaluations/scripts/evaluate-batch.sh) - Bash helper that posts a batch request to `/access/v1/evaluations` (host-pinned; `--print` to preview). ## Agent-specific notes This skill uses generic markdown instructions and works across all agents listed in the [README](/agent-skills/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 - [AuthZEN guide (developer hub)](https://developer.indykite.com/guides/guide-authzen) - [Config API documentation - authorization policies](https://openapi.indykite.com/api-documentation-config#tag/authorization-policies) - [Credentials guide](https://developer.indykite.com/guides/guide-credentials) --- --- name: indykite-authzen-kbac-policies description: Author and manage an IndyKite KBAC (Knowledge-Based Access Control) authorization policy - a single subject type, an actions list, a single resource type, and a Cypher condition over the IKG - through the Config API (`/configs/v1/authorization-policies` - create / read / list `?type=kbac` / update / delete, ETag-guarded). Covers `2.0-kbac` and `3.0-kbac` (raw Cypher, optional location routing for composite / data-residency IKGs). Use to write, publish, inspect, update, or delete a KBAC policy - e.g. "write a policy letting a Person PROVISION a Server within budget", "author a location-routed policy for our composite IKG". This authors the rule; it does NOT make decisions - for "can X do Y on Z?" use indykite-authzen-evaluation (single) or indykite-authzen-evaluations (batch), and to enumerate allowed actions/resources/subjects use indykite-authzen-search-action / -search-resource / -search-subject. This is KBAC, not ContX IQ - for CIQ read/write data policies use the indykite-ciq-* skills. 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 KBAC - authorization policies KBAC (Knowledge-Based Access Control) is IndyKite's graph-driven authorization model. A KBAC **policy** declares *who* (`subject`) may perform *which* operations (`actions`) on *what* (`resource`), gated by a **condition** in Cypher (the Neo4j / openCypher graph query language) evaluated against the IKG: IndyKite's knowledge graph, a property-graph database. The policy itself renders no decision - it is the *rule* that the AuthZEN endpoints consult when a decision or search is requested. This skill is the home of the KBAC **policy lifecycle**: writing the policy JSON and managing it through the Config API. - A **policy** with `meta.policy_version` `"2.0-kbac"` (the default, platform-rewritten Cypher) or `"3.0-kbac"` (raw Cypher, usable on any IKG, with optional location routing for composite IKGs - see [Location-aware policies](#location-aware-policies-for-data-residency-30-kbac)), a single `subject.type`, an `actions` list, a single `resource.type`, and a `condition.cypher` that binds the reserved variables `subject` and `resource`. - The **Config API** operations on `/configs/v1/authorization-policies`: create (`POST`), read (`GET /{id}` or by name), list (`GET ?project_id=…&type=kbac`), update (`PUT /{id}` with an `If-Match` ETag), and delete (`DELETE /{id}`). - Publishing: a policy must be **ACTIVE** to participate in decisions; an `INACTIVE` or `DRAFT` policy is stored but ignored (`DRAFT` may even be invalid). Once a policy is ACTIVE, the runtime AuthZEN skills evaluate it: | Need | Endpoint | Skill | |-------------------------------------------------|------------------------------|-----------------------------------------------------------------------| | One yes/no decision | `/access/v1/evaluation` | [`indykite-authzen-evaluation`](/agent-skills/indykite-authzen-evaluation/SKILL.md) | | Many decisions at once | `/access/v1/evaluations` | [`indykite-authzen-evaluations`](/agent-skills/indykite-authzen-evaluations/SKILL.md) | | Actions a subject may perform on a resource | `/access/v1/search/action` | [`indykite-authzen-search-action`](/agent-skills/indykite-authzen-search-action/SKILL.md) | | Resources a subject may act on, given an action | `/access/v1/search/resource` | [`indykite-authzen-search-resource`](/agent-skills/indykite-authzen-search-resource/SKILL.md) | | Subjects allowed an action on a resource | `/access/v1/search/subject` | [`indykite-authzen-search-subject`](/agent-skills/indykite-authzen-search-subject/SKILL.md) | ## When to use Activate this skill when the user wants to: - **author** a KBAC authorization policy (`policy_version` `2.0-kbac` or `3.0-kbac`, `subject`, `actions`, `resource`, `condition.cypher`); - **author a location-aware policy** for a composite / data-residency IKG (`3.0-kbac` with `USE graph.byName()` routing); - **create / publish** a policy through the Config API, or flip it between `DRAFT` and `ACTIVE`; - **read, list, update, or delete** existing KBAC policies (including listing with `?type=kbac` to separate them from CIQ policies); - needs the policy that backs an [`indykite-authzen-evaluation`](/agent-skills/indykite-authzen-evaluation/SKILL.md) decision or the [`indykite-mcp-server`](/agent-skills/indykite-mcp-server/SKILL.md) `authzen_evaluate` tool. Do **not** activate this skill when the user wants to: - **make a decision** (one triple or many) - use [`indykite-authzen-evaluation`](/agent-skills/indykite-authzen-evaluation/SKILL.md) / [`indykite-authzen-evaluations`](/agent-skills/indykite-authzen-evaluations/SKILL.md); - **enumerate** allowed actions, resources, or subjects - use the search skills [`-search-action`](/agent-skills/indykite-authzen-search-action/SKILL.md) / [`-search-resource`](/agent-skills/indykite-authzen-search-resource/SKILL.md) / [`-search-subject`](/agent-skills/indykite-authzen-search-subject/SKILL.md); - author a **ContX IQ** read/write policy (the same `/configs/v1/authorization-policies` endpoint also serves CIQ, distinguished by `type=ciq`) - use the [`indykite-ciq-*`](/agent-skills/README.md) skills; - **create / update / delete graph data** - a KBAC policy is a rule over the graph, not a write to it. ## Prerequisites - An IndyKite **project**, and the project's GID in `PROJECT_GID` - it becomes the policy's `project_id`. - A **Service Account token** with Config API write access, in `SERVICE_ACCOUNT_TOKEN` - used for every `/configs/v1/authorization-policies` call. - A **subject type** for the policy - the node type making the request (`Person`, `Service`, `Namespace`, etc.). A policy is restricted to a single subject type; if two subject types need the same action, write two policies. - For a `2.0-kbac` policy, the subject nodes must be **identity nodes** - ingested with `is_identity: true` (see [`indykite-capture-upsert-nodes`](/agent-skills/indykite-capture-upsert-nodes/SKILL.md)). A subject ingested as a plain entity never matches a `2.0-kbac` condition, so every decision quietly evaluates to `false`. `3.0-kbac` matches the subject by type and external ID only and does not require `is_identity`. - The **IKG model** the condition will match (node types, properties, relationships). The policy can be authored before the data exists, but a decision over an empty graph is just `false`. For a populated IKG, the exact type and property spellings can be read from the Data Schema API ([`indykite-data-schema`](/agent-skills/indykite-data-schema/SKILL.md)). If any of these are missing, say so before writing JSON. ## Steps ### 1. Frame the rule as (subject, actions, resource, condition) Every KBAC policy answers one shape of question. Pin down all four parts before writing JSON: | Part | What it is | Example | |-------------|------------------------------------------------------------------|---------------------| | `subject` | The single node type making the request. | `Person` | | `actions` | Action names the policy grants (1-5 per policy), conventionally uppercase verbs. | `["PROVISION"]` | | `resource` | The single node type being acted on. | `Server` | | `condition` | A Cypher pattern + `WHERE` that must hold for a decision to be `true`. | price within budget | Working example used throughout this skill: > A `Person` (subject) may `PROVISION` a `Server` (resource) when the server's price is within a budget supplied at evaluation time. ### 2. Write the Cypher condition The condition is a single `cypher` string. Two hard rules: - It **must** bind a variable literally named `subject` (matching `subject.type`) and a variable literally named `resource` (matching `resource.type`). At decision time the AuthZEN request's `subject.id` / `resource.id` are matched against each node's `external_id`. - Anything that varies per request is a **partial parameter** written `$name` in the `WHERE` clause; the decision call supplies it under `context.input_params` (without the `$`). Here the budget is `$max_price`. ```cypher MATCH (subject:Person), (resource:Server) WHERE resource.property.price <= $max_price ``` For relationship-based rules, match the relationship instead of (or in addition to) a property check: ```cypher MATCH (subject:Person)-[:CAN_AFFORD]->(resource:Server) ``` For the full condition grammar (attribute references, multi-hop patterns, partial parameters, and the reserved `$subject_id` parameter that `2.0-kbac` binds to the user token's identity) see [`references/policy-reference.md`](/agent-skills/indykite-authzen-kbac-policies/references/policy-reference.md). ### 3. Assemble the policy and its create envelope A KBAC policy has exactly five top-level keys: `meta` (with `meta.policy_version` set to `"2.0-kbac"` or `"3.0-kbac"`), `subject` (with `subject.type`), `actions`, `resource` (with `resource.type`), and `condition` (required `condition.cypher`; optional `condition.filter`, a graph-free pre-check over token claims and `input_params` - see [`references/policy-reference.md`](/agent-skills/indykite-authzen-kbac-policies/references/policy-reference.md#conditionfilter-optional)). The Config API does not take the policy object directly - it takes a **create envelope** in which the policy is a **stringified** JSON value: ```json { "name": "kbac-person-provision-server", "display_name": "Person: PROVISION a Server within budget", "description": "Allow a Person to PROVISION a Server when its price is within a budget supplied at evaluation time.", "project_id": "", "policy": "{ \"meta\": { \"policy_version\": \"2.0-kbac\" }, … }", "status": "ACTIVE" } ``` For readability the asset [`assets/policy-provision-server.json`](/agent-skills/indykite-authzen-kbac-policies/assets/policy-provision-server.json) keeps `policy` as an object and `project_id` as a placeholder; the create step sets `project_id` and stringifies `policy` just before sending. ### 4. Create the policy ```text POST /configs/v1/authorization-policies ``` Authenticate with the Service Account token: `Authorization: Bearer `. ```bash # set project_id and stringify only the `policy` field, then POST jq --arg pid "$PROJECT_GID" '.project_id = $pid | .policy |= tojson' assets/policy-provision-server.json \ | curl -X POST "$API_URL/configs/v1/authorization-policies" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $SERVICE_ACCOUNT_TOKEN" \ -d @- ``` Or use the helper [`scripts/create-policy.sh`](/agent-skills/indykite-authzen-kbac-policies/scripts/create-policy.sh), which sets `project_id`, stringifies the `policy` field, pins the IndyKite host, and redacts the token under `--print`. A `201 Created` returns the policy's `id` (a `gid:…`), audit fields (`create_time`, `created_by`, …), and an **ETag** header. Keep the `id` and ETag - update and delete need them. The policy participates in decisions only when its `status` is **ACTIVE**. ### 5. Read, list, update, and delete The same `/configs/v1/authorization-policies` path manages the policy lifecycle (all with the Service Account token): - **Read by id**: `GET /configs/v1/authorization-policies/{id}` - returns the full record, including the stringified `policy`, `status`, `tags`, audit fields, and the current ETag. - **Read by name**: `GET /configs/v1/authorization-policies/{name}?location={PROJECT_GID}`. - **List KBAC policies**: `GET /configs/v1/authorization-policies?project_id={PROJECT_GID}&type=kbac` - `type=kbac` returns only KBAC policies (use `type=ciq` for ContX IQ). List responses carry an empty `policy` string per item; read by id to get the body. - **Update**: `PUT /configs/v1/authorization-policies/{id}` with header `If-Match: ` and a body of the fields to change (`display_name`, `description`, `policy`, `status`, `tags`). Use this to publish (`status: "ACTIVE"`), deactivate (`status: "INACTIVE"`), or hold as `DRAFT`, or to revise the condition. A new ETag comes back. - **Delete**: `DELETE /configs/v1/authorization-policies/{id}` with header `If-Match: `. Full request/response shapes, response fields, and the ETag concurrency rules are in [`references/policy-reference.md`](/agent-skills/indykite-authzen-kbac-policies/references/policy-reference.md). ## Location-aware policies for data residency (`3.0-kbac`) On a **composite IKG** - one logical graph spanning multiple constituent databases so that individual nodes can be stored in a specific location (see the [Data Residency guide](https://developer.indykite.com/guides/guide-data-residency)) - a `2.0-kbac` condition always evaluates against the **default database**, where located nodes exist only as lightweight proxies (external ID, type, and location - no property data). To evaluate a condition **inside a location constituent**, author the policy as `3.0-kbac`: - The condition is **raw Cypher**: it runs as authored; the platform only pins `subject` / `resource` by type and external ID and appends the projection. - Route with `USE graph.byName(...)` - **static** (`USE graph.byName('ikcomposite.db2')` always evaluates in that constituent) or **dynamic** (`USE graph.byName($region)`), where `$region` becomes a **location parameter**: at decision time the caller passes a *logical location* (a key of the project's `alias_mapping`, e.g. `"east"`) under `context.input_params`, and IndyKite translates it to the physical constituent just before execution. Callers never see or supply physical database names. - `CALL { }` subqueries with inner `RETURN`s are allowed (each subquery can carry its own `USE` clause), so one condition can combine matches from several constituents. Both are rejected on `2.0-kbac`. - The subject does **not** need to be an identity node: it is matched by type and external ID. Instead, when the decision request carries a user (OAuth bearer) token, the token's subject must be the same identity as the request's `subject`, or the call is denied with `bearer token subject differs from requested subject`. - Conditions referencing **external (resolver-backed) properties** are rejected at creation (`external properties cannot be used in data-residency policies`); they are supported only in `2.0-kbac` conditions. ```json { "meta": { "policy_version": "3.0-kbac" }, "subject": { "type": "Person" }, "actions": ["CAN_DRIVE"], "resource": { "type": "Car" }, "condition": { "cypher": "USE graph.byName($region) MATCH (subject:Person)-[:OWNS]->(resource:Car)" } } ``` The lifecycle is unchanged - same endpoint, create envelope, statuses, and ETag rules as steps 3-5; only the policy JSON differs. Note that `3.0-kbac` itself does not require a composite database - only `USE` routing (static or dynamic) does; a `3.0-kbac` policy without a `USE` clause evaluates on any IKG as plain raw Cypher. Residency support is **opt-in per policy**: existing `2.0-kbac` policies keep working, and a valid `2.0-kbac` condition can be carried over by switching `meta.policy_version` (as long as it references neither `$subject_id` nor external properties, both of which `3.0-kbac` rejects). The full 2.0 vs 3.0 comparison, authoring rules, and decision-time failure modes are in [`references/policy-reference.md`](/agent-skills/indykite-authzen-kbac-policies/references/policy-reference.md#30-kbac-raw-cypher-and-location-routing); a runnable create envelope is in [`assets/policy-location-routed.json`](/agent-skills/indykite-authzen-kbac-policies/assets/policy-location-routed.json). ## Outcome When this skill has been applied successfully: - A KBAC policy exists in the project with `policy_version` `"2.0-kbac"` (or `"3.0-kbac"` for location-aware conditions), a single `subject.type`, an `actions` list, a single `resource.type`, and a `condition.cypher` binding `subject` and `resource`. - The policy is **ACTIVE** (or deliberately `INACTIVE` / `DRAFT`), and its `id` and current ETag are known so it can be read, updated, or deleted. - Listing with `?type=kbac` shows the policy, and an [`indykite-authzen-evaluation`](/agent-skills/indykite-authzen-evaluation/SKILL.md) decision over a matching `(subject, action, resource)` triple reflects it. ## Files in this skill - [`references/policy-reference.md`](/agent-skills/indykite-authzen-kbac-policies/references/policy-reference.md) - KBAC policy schema (`meta`, `subject`, `actions`, `resource`, `condition.cypher`, the optional `condition.filter`, partial parameters, multi-action and relationship variants), the 2.0 vs 3.0 version comparison with `3.0-kbac` routing and authoring rules, and the Config API lifecycle (create / read / list `?type=kbac` / update / delete, ETag concurrency, response fields). - [`assets/policy-provision-server.json`](/agent-skills/indykite-authzen-kbac-policies/assets/policy-provision-server.json) - runnable KBAC policy create envelope for the `Person PROVISION Server` example. - [`assets/policy-location-routed.json`](/agent-skills/indykite-authzen-kbac-policies/assets/policy-location-routed.json) - runnable `3.0-kbac` create envelope with dynamic `USE graph.byName($region)` location routing. - [`scripts/create-policy.sh`](/agent-skills/indykite-authzen-kbac-policies/scripts/create-policy.sh) - Bash helper that sets `project_id`, stringifies `policy`, and POSTs the create envelope to `/configs/v1/authorization-policies` (host-pinned; `--print` to preview). ## Agent-specific notes This skill uses generic markdown instructions and works across all agents listed in the [README](/agent-skills/README.md). The agent needs to be able to issue HTTP requests (`curl`, an HTTP client, or the IndyKite Terraform provider - see References). No Claude Code hooks, Cursor `@`-mentions, or Copilot workspace context are required. ## References - [AuthZEN guide (developer hub)](https://developer.indykite.com/guides/guide-authzen) - [Data Residency guide (developer hub)](https://developer.indykite.com/guides/guide-data-residency) - regions, composite databases, and `3.0-kbac` location routing - [Dynamic authorization with Knowledge Graphs (developer hub)](https://developer.indykite.com/guides/guide-dynamic-authz) - [Config API documentation - authorization policies](https://openapi.indykite.com/api-documentation-config#tag/authorization-policies) - [Cypher query language manual (Neo4j; openCypher)](https://neo4j.com/docs/cypher-manual/current/) - the graph query language used in KBAC policy conditions over the IndyKite Knowledge Graph. - [Music dataset tutorial (worked KBAC policy + AuthZEN example)](https://developer.indykite.com/tutorials/tutorial-music-dataset) - [KBAC recipes (developer hub resources)](https://developer.indykite.com/resources) - [KBAC 3.0: raw-Cypher policies with `CALL { }` subqueries and `USE` routing (authz-7)](https://developer.indykite.com/resources/authz-7) - [KBAC 3.0: location-routed policies for data residency (authz-8)](https://developer.indykite.com/resources/authz-8) - [IndyKite Terraform provider - `indykite_authorization_policy`](https://registry.terraform.io/providers/indykite/indykite/latest/docs) - [Credentials guide](https://developer.indykite.com/guides/guide-credentials) --- --- name: indykite-authzen-search-action description: List the actions a subject is allowed to perform on a resource via the IndyKite AuthZEN REST API (`POST /access/v1/search/action`) - returns the granted action names for one pinned (subject, resource) pair. Use to enumerate permitted operations - "what can linus do with gpu-node-7?", "which actions does this user have on this item?" (e.g. to render only allowed UI controls). Not for a specific-action yes/no ("can linus DEPLOY gpu-node-7?" -> indykite-authzen-evaluation); to enumerate the other axes use indykite-authzen-search-resource (which resources) or indykite-authzen-search-subject (which subjects); to author the policy use indykite-authzen-kbac-policies. 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 AuthZEN - action search Action search asks the AuthZEN endpoint: *which actions may this subject perform on this resource?* It returns the list of granted action names under the project's currently ACTIVE KBAC policies and current graph state - not a single boolean. It is one of three AuthZEN search endpoints, each pinning two of the three `(subject, action, resource)` parts and enumerating the third: | Endpoint | Pinned | Enumerates | Skill | |---------------------------|---------------|------------|-----------------------------------------------------------------------| | `/search/action` | subject + resource | **actions** | this skill | | `/search/resource` | subject + action | resources | [`indykite-authzen-search-resource`](/agent-skills/indykite-authzen-search-resource/SKILL.md) | | `/search/subject` | resource + action | subjects | [`indykite-authzen-search-subject`](/agent-skills/indykite-authzen-search-subject/SKILL.md) | This skill covers building and sending the request and reading the results. It does **not** author policies - the `2.0-kbac` policies whose `actions` these results come from are authored with [`indykite-authzen-kbac-policies`](/agent-skills/indykite-authzen-kbac-policies/SKILL.md). ## When to use Activate this skill when the user wants to: - list **every action a subject may perform on a specific resource** (e.g. "what can `linus` do with `gpu-node-7`?"); - drive a UI that shows only the operations a user is currently permitted on an item; - debug why an expected action is or is not granted for a `(subject, resource)` pair. Do **not** activate this skill for a single yes/no **decision** ([`indykite-authzen-evaluation`](/agent-skills/indykite-authzen-evaluation/SKILL.md)), to enumerate **resources** or **subjects** instead (the sibling search skills), to run **many decisions** at once ([`indykite-authzen-evaluations`](/agent-skills/indykite-authzen-evaluations/SKILL.md)), or to **author a policy** or **read/write graph data** (search only lists actions). ## Prerequisites - One or more **ACTIVE KBAC policies** whose `subject.type` / `resource.type` match the pair you are asking about. If none exist, author them first with [`indykite-authzen-kbac-policies`](/agent-skills/indykite-authzen-kbac-policies/SKILL.md); search over an empty policy set returns `{"results": []}`. - An **AppAgent** with credentials configured for the calling application ([Credentials guide](https://developer.indykite.com/guides/guide-credentials)). - The **IKG populated** with the subject and resource nodes (and any relationships the policy conditions match). Search evaluates the graph; it does not seed it. - Any **partial parameters** a candidate policy references, ready to pass under `context.input_params`. If a prerequisite is missing, say so - an empty result set from a missing policy or absent node looks identical to a real "nothing permitted". ## Steps ### 1. Pin the subject and the resource Action search fixes **both** the subject and the resource and asks what is allowed between them. Identify each by its node `external_id`: | Part | Field | Example | |------------|------------------|--------------------------| | subject | `subject.type` + `subject.id` | `Person` / `linus` | | resource | `resource.type` + `resource.id` | `Server` / `gpu-node-7` | There is no `action` field - discovering the actions is the point. ### 2. Build the request body ```json { "subject": { "type": "Person", "id": "linus" }, "resource": { "type": "Server", "id": "gpu-node-7" }, "context": { "input_params": { "max_price": 120000 } } } ``` Include `context.input_params` only if a candidate policy's condition references a `$name` partial parameter; supply each key **without** the leading `$`, with the correct type (numbers stay numbers). A ready body: [`assets/search-action-request.json`](/agent-skills/indykite-authzen-search-action/assets/search-action-request.json). ### 3. Send the search ```text POST /access/v1/search/action ``` The endpoint authenticates the **calling application** (its AppAgent credentials - always required) and **optionally the user** (an access token - applies only in some cases; when supplied it can narrow the results). Which credential goes in which request header is covered by the [Credentials guide](https://developer.indykite.com/guides/guide-credentials). A runnable shell helper builds the authenticated request: [`scripts/search-action.sh`](/agent-skills/indykite-authzen-search-action/scripts/search-action.sh) — run with `--print` to preview the `curl` (host-pinned; tokens redacted). ### 4. Read the results ```json { "results": [ { "name": "DEPLOY" }, { "name": "REBOOT" }, { "name": "SNAPSHOT" } ] } ``` Each `results[]` entry is an action the subject may currently perform on that resource. An **empty** `results` array is a normal `200` meaning nothing is granted - not an error. ### 5. Verify Empty or surprising results usually trace to: the policy isn't **ACTIVE**/matching, `subject.id`/`resource.id` aren't the nodes' `external_id`s, a required `$param` is missing or mistyped, the graph data is absent, or a supplied user token narrowed claim-gated actions. Full checklist: [`references/search-action-reference.md`](/agent-skills/indykite-authzen-search-action/references/search-action-reference.md). ## Outcome When this skill has been applied successfully: - `POST /access/v1/search/action` returns a `results` array of the actions a subject may perform on a specific resource under current ACTIVE policies and graph state (an empty array means nothing permitted — a normal `200`). - The actions returned line up with the `actions` of the KBAC policies authored via [`indykite-authzen-kbac-policies`](/agent-skills/indykite-authzen-kbac-policies/SKILL.md). ## Files in this skill - [`references/search-action-reference.md`](/agent-skills/indykite-authzen-search-action/references/search-action-reference.md) - endpoint, auth, request/response shape, error codes, troubleshooting, sibling endpoints. - [`assets/search-action-request.json`](/agent-skills/indykite-authzen-search-action/assets/search-action-request.json) - runnable action-search request body for the `linus` / `gpu-node-7` example. - [`scripts/search-action.sh`](/agent-skills/indykite-authzen-search-action/scripts/search-action.sh) - Bash helper that posts a search request to `/access/v1/search/action` (host-pinned; `--print` to preview). ## Agent-specific notes This skill uses generic markdown instructions and works across all agents listed in the [README](/agent-skills/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 - [AuthZEN guide (developer hub)](https://developer.indykite.com/guides/guide-authzen) - [Config API documentation - authorization policies](https://openapi.indykite.com/api-documentation-config#tag/authorization-policies) - [Credentials guide](https://developer.indykite.com/guides/guide-credentials) --- --- name: indykite-authzen-search-resource description: List the resources a subject is allowed to perform a given action on via the IndyKite AuthZEN REST API (`POST /access/v1/search/resource`) - given a subject and an action, returns the matching resource instances of a type. Use to enumerate permitted resources - "which servers can linus provision?", "list the documents this user can read" (access-filtered feeds). Returns `{type,id}` references, not a yes/no decision and not the resource data itself (for graph data use indykite-ciq-read). For a single-resource yes/no use indykite-authzen-evaluation; to enumerate the other axes use indykite-authzen-search-action (which actions) or indykite-authzen-search-subject (which subjects); to author the policy use indykite-authzen-kbac-policies. 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 AuthZEN - resource search Resource search asks the AuthZEN endpoint: *which resources may this subject perform a given action on?* Given one subject, one action, and a resource **type**, it returns the matching resource instances under the project's currently ACTIVE KBAC policies and current graph state. It is one of three AuthZEN search endpoints, each pinning two of the three `(subject, action, resource)` parts and enumerating the third: | Endpoint | Pinned | Enumerates | Skill | |---------------------------|---------------|------------|---------------------------------------------------------------------| | `/search/action` | subject + resource | actions | [`indykite-authzen-search-action`](/agent-skills/indykite-authzen-search-action/SKILL.md) | | `/search/resource` | subject + action | **resources** | this skill | | `/search/subject` | resource + action | subjects | [`indykite-authzen-search-subject`](/agent-skills/indykite-authzen-search-subject/SKILL.md) | This skill covers building and sending the request and reading the results. It does **not** author policies - the `2.0-kbac` policies these results are evaluated against are authored with [`indykite-authzen-kbac-policies`](/agent-skills/indykite-authzen-kbac-policies/SKILL.md). ## When to use Activate this skill when the user wants to: - list **every resource of a type a subject may act on** under one action (e.g. "which servers can `linus` `PROVISION`?", "which documents can this user read?"); - build a list/feed filtered to the items a user is permitted to act on; - debug why an expected resource is or is not in a subject's permitted set. Do **not** activate this skill for a single yes/no **decision** ([`indykite-authzen-evaluation`](/agent-skills/indykite-authzen-evaluation/SKILL.md)), to enumerate **actions** or **subjects** instead (the sibling search skills), or to **author a policy** or **read/write graph data** (search only lists resources). ## Prerequisites - One or more **ACTIVE KBAC policies** whose `subject.type` / `resource.type` and `actions` cover the question. If none exist, author them first with [`indykite-authzen-kbac-policies`](/agent-skills/indykite-authzen-kbac-policies/SKILL.md); search over an empty policy set returns `{"results": []}`. - An **AppAgent** with credentials configured for the calling application ([Credentials guide](https://developer.indykite.com/guides/guide-credentials)). - The **IKG populated** with the subject and candidate resource nodes (and any relationships the policy conditions match). - Any **partial parameters** a candidate policy references, ready to pass under `context.input_params`. If a prerequisite is missing, say so - an empty result set from a missing policy or absent node looks identical to a real "nothing permitted". ## Steps ### 1. Pin the subject and the action; leave the resource a type Resource search fixes the **subject** and the **action**, and searches across resources of a given **type**: | Part | Field | Example | |------------|--------------------------------|--------------------------| | subject | `subject.type` + `subject.id` | `Person` / `linus` | | action | `action.name` | `PROVISION` | | resource | `resource.type` **only** | `Server` (no `id`) | Do **not** set `resource.id` - the instances are what the search returns. ### 2. Build the request body ```json { "subject": { "type": "Person", "id": "linus" }, "resource": { "type": "Server" }, "action": { "name": "PROVISION" }, "context": { "input_params": { "max_price": 4000 } } } ``` Include `context.input_params` only if a candidate policy references a `$name` partial parameter; supply each key **without** the `$`, correctly typed. A ready body: [`assets/search-resource-request.json`](/agent-skills/indykite-authzen-search-resource/assets/search-resource-request.json). ### 3. Send the search ```text POST /access/v1/search/resource ``` The endpoint authenticates the **calling application** (its AppAgent credentials - always required) and **optionally the user** (an access token - applies only in some cases; when supplied it can narrow the results). Which credential goes in which request header is covered by the [Credentials guide](https://developer.indykite.com/guides/guide-credentials). A runnable shell helper builds the authenticated request: [`scripts/search-resource.sh`](/agent-skills/indykite-authzen-search-resource/scripts/search-resource.sh) — run with `--print` to preview the `curl` (host-pinned; tokens redacted). ### 4. Read the results ```json { "results": [ { "type": "Server", "id": "edge-box-2" } ] } ``` Each `results[]` entry is a resource (`type` + `id`, the `external_id`) the subject may perform the action on. An **empty** `results` array is a normal `200` meaning nothing of that type is permitted - not an error. ### 5. Verify Empty or surprising results usually trace to: `resource.id` accidentally set (it takes `resource.type` only), the `action` not matching a policy, `subject.id` not being an `external_id`, a `$param` missing/mistyped (tightening `max_price` shrinks the set), or the resource nodes being absent. Full checklist: [`references/search-resource-reference.md`](/agent-skills/indykite-authzen-search-resource/references/search-resource-reference.md). ## Outcome When this skill has been applied successfully: - `POST /access/v1/search/resource` returns a `results` array of resource instances (`type` + `id`) a subject may perform the given action on, under current ACTIVE policies and graph state (an empty array means nothing of that type permitted — a normal `200`). - The resources returned are consistent with single-decision (`/access/v1/evaluation`) results for the same triples. ## Files in this skill - [`references/search-resource-reference.md`](/agent-skills/indykite-authzen-search-resource/references/search-resource-reference.md) - endpoint, auth, request/response shape, error codes, troubleshooting, sibling endpoints. - [`assets/search-resource-request.json`](/agent-skills/indykite-authzen-search-resource/assets/search-resource-request.json) - runnable resource-search request body for the `linus` `PROVISION` `Server` example. - [`scripts/search-resource.sh`](/agent-skills/indykite-authzen-search-resource/scripts/search-resource.sh) - Bash helper that posts a search request to `/access/v1/search/resource` (host-pinned; `--print` to preview). ## Agent-specific notes This skill uses generic markdown instructions and works across all agents listed in the [README](/agent-skills/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 - [AuthZEN guide (developer hub)](https://developer.indykite.com/guides/guide-authzen) - [Config API documentation - authorization policies](https://openapi.indykite.com/api-documentation-config#tag/authorization-policies) - [Credentials guide](https://developer.indykite.com/guides/guide-credentials) --- --- name: indykite-authzen-search-subject description: List the subjects allowed to perform a given action on a resource via the IndyKite AuthZEN REST API (`POST /access/v1/search/subject`) - given a resource and an action, returns the matching subject instances of a type. Use to enumerate who has access - "who can provision gpu-node-7?", "list the people allowed to approve this document" (audit / reviewer views). Not for a specific-subject yes/no ("can grace provision gpu-node-7?" -> indykite-authzen-evaluation); to enumerate the other axes use indykite-authzen-search-action (which actions) or indykite-authzen-search-resource (which resources); to author the policy use indykite-authzen-kbac-policies. 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 AuthZEN - subject search Subject search asks the AuthZEN endpoint: *which subjects may perform a given action on this resource?* Given a subject **type**, one resource, and one action, it returns the matching subject instances under the project's currently ACTIVE KBAC policies and current graph state. It is one of three AuthZEN search endpoints, each pinning two of the three `(subject, action, resource)` parts and enumerating the third: | Endpoint | Pinned | Enumerates | Skill | |---------------------------|---------------|------------|---------------------------------------------------------------------| | `/search/action` | subject + resource | actions | [`indykite-authzen-search-action`](/agent-skills/indykite-authzen-search-action/SKILL.md) | | `/search/resource` | subject + action | resources | [`indykite-authzen-search-resource`](/agent-skills/indykite-authzen-search-resource/SKILL.md) | | `/search/subject` | resource + action | **subjects** | this skill | This skill covers building and sending the request and reading the results. It does **not** author policies - the `2.0-kbac` policies these results are evaluated against are authored with [`indykite-authzen-kbac-policies`](/agent-skills/indykite-authzen-kbac-policies/SKILL.md). ## When to use Activate this skill when the user wants to: - list **every subject of a type that may perform an action on a specific resource** (e.g. "who can `DEPLOY` `gpu-node-7`?", "who can approve this document?"); - build an audit or reviewer view of who currently has access to an item; - debug why an expected subject is or is not in a resource's permitted set. Do **not** activate this skill for a single yes/no **decision** ([`indykite-authzen-evaluation`](/agent-skills/indykite-authzen-evaluation/SKILL.md)), to enumerate **actions** or **resources** instead (the sibling search skills), or to **author a policy** or **read/write graph data** (search only lists subjects). ## Prerequisites - One or more **ACTIVE KBAC policies** whose `subject.type` / `resource.type` and `actions` cover the question. If none exist, author them first with [`indykite-authzen-kbac-policies`](/agent-skills/indykite-authzen-kbac-policies/SKILL.md); search over an empty policy set returns `{"results": []}`. - An **AppAgent** with credentials configured for the calling application ([Credentials guide](https://developer.indykite.com/guides/guide-credentials)). - The **IKG populated** with the candidate subject nodes and the resource node (and any relationships the policy conditions match). - Any **partial parameters** a candidate policy references, ready to pass under `context.input_params`. If a prerequisite is missing, say so - an empty result set from a missing policy or absent node looks identical to a real "nothing permitted". ## Steps ### 1. Pin the resource and the action; leave the subject a type Subject search fixes the **resource** and the **action**, and searches across subjects of a given **type**: | Part | Field | Example | |------------|--------------------------------|------------------| | subject | `subject.type` **only** | `Person` (no `id`) | | action | `action.name` | `PROVISION` | | resource | `resource.type` + `resource.id`| `Server` / `gpu-node-7` | Do **not** set `subject.id` - the subjects are what the search returns. ### 2. Build the request body ```json { "subject": { "type": "Person" }, "resource": { "type": "Server", "id": "gpu-node-7" }, "action": { "name": "PROVISION" }, "context": { "input_params": { "max_price": 80000 } } } ``` Include `context.input_params` only if a candidate policy references a `$name` partial parameter; supply each key **without** the `$`, correctly typed. A ready body: [`assets/search-subject-request.json`](/agent-skills/indykite-authzen-search-subject/assets/search-subject-request.json). ### 3. Send the search ```text POST /access/v1/search/subject ``` The endpoint authenticates the **calling application** (its AppAgent credentials - always required). A **user access token** is accepted too, but for subject search it typically has **no effect** on the result set (you are enumerating subjects, not acting as one). Which credential goes in which request header is covered by the [Credentials guide](https://developer.indykite.com/guides/guide-credentials). A runnable shell helper builds the authenticated request: [`scripts/search-subject.sh`](/agent-skills/indykite-authzen-search-subject/scripts/search-subject.sh) — run with `--print` to preview the `curl` (host-pinned; tokens redacted). ### 4. Read the results ```json { "results": [ { "type": "Person", "id": "grace" }, { "type": "Person", "id": "dennis" } ] } ``` Each `results[]` entry is a subject (`type` + `id`, the `external_id`) allowed the action on that resource. An **empty** `results` array is a normal `200` meaning no subject of that type is permitted - not an error. ### 5. Verify Empty or surprising results usually trace to: `subject.id` accidentally set (it takes `subject.type` only), `resource.id` not being an `external_id`, the `action` not matching a policy, a `$param` missing/mistyped (loosening `max_price` widens the set), or the subject nodes being absent. Full checklist: [`references/search-subject-reference.md`](/agent-skills/indykite-authzen-search-subject/references/search-subject-reference.md). ## Outcome When this skill has been applied successfully: - `POST /access/v1/search/subject` returns a `results` array of subject instances (`type` + `id`) allowed a given action on a specific resource, under current ACTIVE policies and graph state (an empty array means no subject of that type permitted — a normal `200`). - The subjects returned are consistent with single-decision (`/access/v1/evaluation`) results for the same triples. ## Files in this skill - [`references/search-subject-reference.md`](/agent-skills/indykite-authzen-search-subject/references/search-subject-reference.md) - endpoint, auth, request/response shape, error codes, troubleshooting, sibling endpoints. - [`assets/search-subject-request.json`](/agent-skills/indykite-authzen-search-subject/assets/search-subject-request.json) - runnable subject-search request body for the "who can `PROVISION` `gpu-node-7`" example. - [`scripts/search-subject.sh`](/agent-skills/indykite-authzen-search-subject/scripts/search-subject.sh) - Bash helper that posts a search request to `/access/v1/search/subject` (host-pinned; `--print` to preview). ## Agent-specific notes This skill uses generic markdown instructions and works across all agents listed in the [README](/agent-skills/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 - [AuthZEN guide (developer hub)](https://developer.indykite.com/guides/guide-authzen) - [Config API documentation - authorization policies](https://openapi.indykite.com/api-documentation-config#tag/authorization-policies) - [Credentials guide](https://developer.indykite.com/guides/guide-credentials) --- --- name: indykite-capture-delete-node-properties description: Build the request-body JSON for the IndyKite Capture API batch node-property delete (`POST /capture/v1/nodes/properties/delete`) - a `nodes` array (1-250 per request) where each entry names a node (`external_id` + `type`) and the `property_types` (1-250 names) to strip from it; the node itself survives. On composite IKGs an optional per-node `location` routes the delete. Use when the user wants to remove specific properties from entities in the IndyKite Knowledge Graph (IKG) - "drop the email property from millicent", "strip these deprecated fields from all listed devices", "prepare a property-delete payload for the Capture API". Produces a ready-to-send JSON file; sending it is optional. Not for deleting whole nodes (indykite-capture-delete-nodes), property metadata only (indykite-capture-delete-node-property-metadata), relationship properties (indykite-capture-delete-relationship-properties), or CIQ policy-mediated deletes (indykite-ciq-delete). license: Apache-2.0 compatibility: Requires curl, bash 4+, and jq for the bundled helper script; authoring the JSON payload itself needs no tools. Network access to the regional IndyKite REST API (eu.api.indykite.com or us.api.indykite.com) is required at runtime. --- # IndyKite Capture - delete node properties This skill builds the request body for the Capture API's **batch node-property delete** endpoint - removing named properties from nodes in the IndyKite Knowledge Graph (IKG) while keeping the nodes themselves: ```text POST /capture/v1/nodes/properties/delete ``` Each entry names one node by (`type`, `external_id`) and lists the `property_types` to strip. The JSON file is the deliverable, ready to be POSTed by any application. The [MCP server](/agent-skills/indykite-mcp-server/SKILL.md) does not currently expose Capture endpoints; the JSON bodies this skill produces are for direct REST use and remain valid if Capture tools are added later. ## When to use Activate this skill when the user wants to: - **remove specific properties** from a node - PII minimization, dropping deprecated fields, correcting a wrong ingest - while keeping the node; - strip the same property set from **many nodes** in one call. Do **not** activate this skill to: - delete the **whole node** - use [`indykite-capture-delete-nodes`](/agent-skills/indykite-capture-delete-nodes/SKILL.md); - remove only a property's **metadata** (value survives) - use [`indykite-capture-delete-node-property-metadata`](/agent-skills/indykite-capture-delete-node-property-metadata/SKILL.md); - remove **relationship** properties - use [`indykite-capture-delete-relationship-properties`](/agent-skills/indykite-capture-delete-relationship-properties/SKILL.md); - overwrite a property with a new value - just re-upsert it with [`indykite-capture-upsert-nodes`](/agent-skills/indykite-capture-upsert-nodes/SKILL.md); - delete through a **CIQ policy + Knowledge Query** - use [`indykite-ciq-delete`](/agent-skills/indykite-ciq-delete/SKILL.md). ## Prerequisites - An IndyKite **project** with an **AppAgent** whose credentials are configured for the calling application ([Credentials guide](https://developer.indykite.com/guides/guide-credentials)). - The (`type`, `external_id`) of each node and the exact **property names** to remove. ## Steps ### 1. List the nodes and the properties to strip | Field | Required | Constraints | Meaning | |------------------|----------|--------------|-----------------------------------------------------------| | `external_id` | yes | 1-256 chars | The node's caller-owned identifier. | | `type` | yes | 2-64 chars | The node's type. | | `property_types` | yes | 1-250 names | The property names to delete from this node. | | `location` | no | 2-32 chars | Composite IKG only: the node's logical location. Omit on a regular IKG. | ### 2. Assemble the request body One JSON object: `{ "nodes": [ … ] }`, 1-250 entries per request. A ready example: [`assets/delete-node-properties.json`](/agent-skills/indykite-capture-delete-node-properties/assets/delete-node-properties.json). ```json { "nodes": [ { "external_id": "millicent", "type": "Person", "property_types": ["email", "given_name"] } ] } ``` This file is the deliverable - any HTTP-capable application can send it. ### 3. Send it (optional) The endpoint authenticates the **calling application** (its AppAgent credentials). Which credential goes in which request header is covered by the [Credentials guide](https://developer.indykite.com/guides/guide-credentials). A runnable shell helper builds the authenticated request: [`scripts/capture.sh`](/agent-skills/indykite-capture-delete-node-properties/scripts/capture.sh) — run with `--print` to preview the `curl` (host-pinned; token redacted). Deletion is destructive: preview with `--print`, and confirm the property list, before sending. ### 4. Read the results A `200` returns one result per node, in order: `{ "results": [ { "id": "gid:…" } ] }`. Field shapes and error semantics: [`references/capture-reference.md`](/agent-skills/indykite-capture-delete-node-properties/references/capture-reference.md). ## Outcome - A valid `{ "nodes": [ … ] }` body exists, each entry naming a node and its `property_types` to remove. - If sent, the listed properties are gone from those nodes; the nodes, their other properties, and their relationships are untouched. ## Files in this skill - [`references/capture-reference.md`](/agent-skills/indykite-capture-delete-node-properties/references/capture-reference.md) - field reference, batch limits, and error semantics. - [`assets/delete-node-properties.json`](/agent-skills/indykite-capture-delete-node-properties/assets/delete-node-properties.json) - ready request body stripping two properties from a `Person`. - [`scripts/capture.sh`](/agent-skills/indykite-capture-delete-node-properties/scripts/capture.sh) - Bash helper that POSTs a body file to `/capture/v1/nodes/properties/delete` (host-pinned; `--print` to preview). ## Agent-specific notes This skill uses generic markdown instructions and works across all agents listed in the [README](/agent-skills/README.md). The agent needs to be able to write a JSON file; sending it additionally requires HTTP access (`curl` or any HTTP client). ## References - [Capture API reference (OpenAPI)](https://openapi.indykite.com/) - [Data Residency guide](https://developer.indykite.com/guides/guide-data-residency) - location-aware deletes on composite IKGs - [Credentials guide](https://developer.indykite.com/guides/guide-credentials) --- --- name: indykite-capture-delete-node-property-metadata description: Build the request-body JSON for the IndyKite Capture API batch property-metadata delete (`POST /capture/v1/nodes/properties/metadata/delete`) - a `nodes` array (1-250 per request) where each entry names a node (`external_id` + `type`), one `property_type`, and the `metadata_fields` (1-250, e.g. source, assurance_level, verified_time, custom_metadata) to remove from that property; the property and its value survive. Use when the user wants to strip provenance metadata in the IndyKite Knowledge Graph (IKG) - "remove the assurance level from millicent's name property", "clear the verified_time metadata on these records", "prepare a metadata-delete payload for the Capture API". Produces a ready-to-send JSON file; sending it is optional. Not for deleting the property itself (indykite-capture-delete-node-properties), whole nodes (indykite-capture-delete-nodes), or attaching metadata (indykite-capture-upsert-nodes re-upserts it). license: Apache-2.0 compatibility: Requires curl, bash 4+, and jq for the bundled helper script; authoring the JSON payload itself needs no tools. Network access to the regional IndyKite REST API (eu.api.indykite.com or us.api.indykite.com) is required at runtime. --- # IndyKite Capture - delete node property metadata This skill builds the request body for the Capture API's **batch property-metadata delete** endpoint - removing metadata fields (provenance such as `source`, `assurance_level`, `verified_time`, `custom_metadata`) from a node property in the IndyKite Knowledge Graph (IKG), while keeping the property and its value: ```text POST /capture/v1/nodes/properties/metadata/delete ``` Each entry names one node by (`type`, `external_id`), one `property_type` on it, and the `metadata_fields` to strip. The JSON file is the deliverable, ready to be POSTed by any application. The [MCP server](/agent-skills/indykite-mcp-server/SKILL.md) does not currently expose Capture endpoints; the JSON bodies this skill produces are for direct REST use and remain valid if Capture tools are added later. ## When to use Activate this skill when the user wants to: - **strip provenance metadata** from a property - e.g. drop a stale `verified_time` or an obsolete `source` - without touching the property value; - clean metadata that feeds [trust scoring](https://developer.indykite.com/guides/guide-trust-score) so a factor no longer contributes. Do **not** activate this skill to: - delete the **property itself** (value and all) - use [`indykite-capture-delete-node-properties`](/agent-skills/indykite-capture-delete-node-properties/SKILL.md); - delete the **whole node** - use [`indykite-capture-delete-nodes`](/agent-skills/indykite-capture-delete-nodes/SKILL.md); - **set or update** metadata - re-upsert the property with a `metadata` object via [`indykite-capture-upsert-nodes`](/agent-skills/indykite-capture-upsert-nodes/SKILL.md). ## Prerequisites - An IndyKite **project** with an **AppAgent** whose credentials are configured for the calling application ([Credentials guide](https://developer.indykite.com/guides/guide-credentials)). - For each target: the node's (`type`, `external_id`), the **property name**, and the **metadata field names** to remove. ## Steps ### 1. List the targets | Field | Required | Constraints | Meaning | |-------------------|----------|-------------|----------------------------------------------------------------| | `external_id` | yes | 1-256 chars | The node's caller-owned identifier. | | `type` | yes | 2-64 chars | The node's type. | | `property_type` | yes | string | The single property whose metadata is being removed. | | `metadata_fields` | yes | 1-250 names | Metadata fields to remove - the fields a property's `metadata` object can carry are `source`, `assurance_level`, `verified_time`, and `custom_metadata`. | | `location` | no | 2-32 chars | Composite IKG only: the node's logical location. Omit on a regular IKG. | One entry addresses **one property**; to clean several properties on the same node, add one entry per property. ### 2. Assemble the request body One JSON object: `{ "nodes": [ … ] }`, 1-250 entries per request. A ready example: [`assets/delete-property-metadata.json`](/agent-skills/indykite-capture-delete-node-property-metadata/assets/delete-property-metadata.json). ```json { "nodes": [ { "external_id": "millicent", "type": "Person", "property_type": "name", "metadata_fields": ["assurance_level", "source"] } ] } ``` This file is the deliverable - any HTTP-capable application can send it. ### 3. Send it (optional) The endpoint authenticates the **calling application** (its AppAgent credentials). Which credential goes in which request header is covered by the [Credentials guide](https://developer.indykite.com/guides/guide-credentials). A runnable shell helper builds the authenticated request: [`scripts/capture.sh`](/agent-skills/indykite-capture-delete-node-property-metadata/scripts/capture.sh) — run with `--print` to preview the `curl` (host-pinned; token redacted). ### 4. Read the results A `200` returns one result per entry, in order: `{ "results": [ { "id": "gid:…" } ] }`. Field shapes and error semantics: [`references/capture-reference.md`](/agent-skills/indykite-capture-delete-node-property-metadata/references/capture-reference.md). ## Outcome - A valid `{ "nodes": [ … ] }` body exists, each entry naming a node, one `property_type`, and the `metadata_fields` to remove. - If sent, those metadata fields are gone from the property; the property value, the node, and everything else are untouched. ## Files in this skill - [`references/capture-reference.md`](/agent-skills/indykite-capture-delete-node-property-metadata/references/capture-reference.md) - field reference, the metadata field names, batch limits, and error semantics. - [`assets/delete-property-metadata.json`](/agent-skills/indykite-capture-delete-node-property-metadata/assets/delete-property-metadata.json) - ready request body stripping `assurance_level` and `source` from a property. - [`scripts/capture.sh`](/agent-skills/indykite-capture-delete-node-property-metadata/scripts/capture.sh) - Bash helper that POSTs a body file to `/capture/v1/nodes/properties/metadata/delete` (host-pinned; `--print` to preview). ## Agent-specific notes This skill uses generic markdown instructions and works across all agents listed in the [README](/agent-skills/README.md). The agent needs to be able to write a JSON file; sending it additionally requires HTTP access (`curl` or any HTTP client). ## References - [Capture API reference (OpenAPI)](https://openapi.indykite.com/) - [Trust Score guide](https://developer.indykite.com/guides/guide-trust-score) - how property metadata feeds scoring - [Credentials guide](https://developer.indykite.com/guides/guide-credentials) --- --- name: indykite-capture-delete-nodes description: Build the request-body JSON for the IndyKite Capture API batch node delete (`POST /capture/v1/nodes/delete`) - a `nodes` array (1-250 per request) of `{external_id, type}` references, each removing one whole node from the IndyKite Knowledge Graph (IKG); on composite IKGs an optional per-node `location` routes the delete to the right constituent. Use when the user wants to remove entities - "delete these test people from the graph", "remove the car kitt", "prepare a node-delete payload for the Capture API". Produces a ready-to-send JSON file; sending it is optional. Not for removing individual properties (indykite-capture-delete-node-properties), property metadata (indykite-capture-delete-node-property-metadata), relationships (indykite-capture-delete-relationships), or CIQ policy-mediated deletes (indykite-ciq-delete). license: Apache-2.0 compatibility: Requires curl, bash 4+, and jq for the bundled helper script; authoring the JSON payload itself needs no tools. Network access to the regional IndyKite REST API (eu.api.indykite.com or us.api.indykite.com) is required at runtime. --- # IndyKite Capture - delete nodes This skill builds the request body for the Capture API's **batch node delete** endpoint - removing whole nodes from the IndyKite Knowledge Graph (IKG): ```text POST /capture/v1/nodes/delete ``` Each entry references one node by (`type`, `external_id`) - the same pair that identified it at upsert. The JSON file is the deliverable, ready to be POSTed by any application. The [MCP server](/agent-skills/indykite-mcp-server/SKILL.md) does not currently expose Capture endpoints; the JSON bodies this skill produces are for direct REST use and remain valid if Capture tools are added later. ## When to use Activate this skill when the user wants to: - **remove entities** from the IKG - cleanup of test data, offboarding a record, retiring a device; - undo a batch ingested with [`indykite-capture-upsert-nodes`](/agent-skills/indykite-capture-upsert-nodes/SKILL.md). Do **not** activate this skill to: - remove **individual properties** (node survives) - use [`indykite-capture-delete-node-properties`](/agent-skills/indykite-capture-delete-node-properties/SKILL.md); - remove **property metadata** (property survives) - use [`indykite-capture-delete-node-property-metadata`](/agent-skills/indykite-capture-delete-node-property-metadata/SKILL.md); - remove **relationships** - use [`indykite-capture-delete-relationships`](/agent-skills/indykite-capture-delete-relationships/SKILL.md); - delete through a **CIQ policy + Knowledge Query** (parameterized, authorization-gated deletes) - use [`indykite-ciq-delete`](/agent-skills/indykite-ciq-delete/SKILL.md). ## Prerequisites - An IndyKite **project** with an **AppAgent** whose credentials are configured for the calling application ([Credentials guide](https://developer.indykite.com/guides/guide-credentials)). - The (`type`, `external_id`) pairs of the nodes to remove. ## Steps ### 1. List the nodes to delete | Field | Required | Constraints | Meaning | |---------------|----------|-------------|-----------------------------------------------------| | `external_id` | yes | 1-256 chars | The node's caller-owned identifier. | | `type` | yes | 2-64 chars | The node's type. | | `location` | no | 2-32 chars | Composite IKG only: the node's logical location (an `alias_mapping` key). Deleting a located node removes both its data node and its global proxy. Omit on a regular IKG. | ### 2. Assemble the request body One JSON object: `{ "nodes": [ … ] }`, 1-250 entries per request. A ready example: [`assets/delete-nodes.json`](/agent-skills/indykite-capture-delete-nodes/assets/delete-nodes.json). ```json { "nodes": [ { "external_id": "kitt", "type": "Car" }, { "external_id": "ryan", "type": "Person" } ] } ``` This file is the deliverable - any HTTP-capable application can send it. ### 3. Send it (optional) The endpoint authenticates the **calling application** (its AppAgent credentials). Which credential goes in which request header is covered by the [Credentials guide](https://developer.indykite.com/guides/guide-credentials). A runnable shell helper builds the authenticated request: [`scripts/capture.sh`](/agent-skills/indykite-capture-delete-nodes/scripts/capture.sh) — run with `--print` to preview the `curl` (host-pinned; token redacted). Deletion is destructive: preview with `--print`, and confirm the target list, before sending. ### 4. Read the results A `200` returns one result per node, in order: `{ "results": [ { "id": "gid:…" } ] }`. Field shapes and error semantics: [`references/capture-reference.md`](/agent-skills/indykite-capture-delete-nodes/references/capture-reference.md). ## Outcome - A valid `{ "nodes": [ … ] }` delete body exists, each entry a (`type`, `external_id`) reference. - If sent, the nodes are gone from the IKG and no longer appear in CIQ query results or KBAC decisions. ## Files in this skill - [`references/capture-reference.md`](/agent-skills/indykite-capture-delete-nodes/references/capture-reference.md) - field reference, composite-IKG `location` behavior, batch limits, and error semantics. - [`assets/delete-nodes.json`](/agent-skills/indykite-capture-delete-nodes/assets/delete-nodes.json) - ready request body removing a `Car` and a `Person`. - [`scripts/capture.sh`](/agent-skills/indykite-capture-delete-nodes/scripts/capture.sh) - Bash helper that POSTs a body file to `/capture/v1/nodes/delete` (host-pinned; `--print` to preview). ## Agent-specific notes This skill uses generic markdown instructions and works across all agents listed in the [README](/agent-skills/README.md). The agent needs to be able to write a JSON file; sending it additionally requires HTTP access (`curl` or any HTTP client). ## References - [Capture API reference (OpenAPI)](https://openapi.indykite.com/) - [Data Residency guide](https://developer.indykite.com/guides/guide-data-residency) - location-aware deletes on composite IKGs - [Credentials guide](https://developer.indykite.com/guides/guide-credentials) --- --- name: indykite-capture-delete-relationship-properties description: Build the request-body JSON for the IndyKite Capture API batch relationship-property delete (`POST /capture/v1/relationships/properties/delete`) - a `relationships` array (1-250 per request), each entry identifying a relationship by `source` node, `target` node (each `external_id` + `type`), and relationship `type`, plus the `property_types` (1-250 names) to strip from it; the relationship itself survives. On composite IKGs setting the top-level `use_global_db` field to `true` targets the global constituent. Use when the user wants to remove properties from edges in the IndyKite Knowledge Graph (IKG) - "drop the status property from millicent's OWNS edge", "prepare a relationship-property-delete payload for the Capture API". Produces a ready-to-send JSON file; sending it is optional. Not for deleting the relationship (indykite-capture-delete-relationships), node properties (indykite-capture-delete-node-properties), or CIQ policy-mediated deletes (indykite-ciq-delete). license: Apache-2.0 compatibility: Requires curl, bash 4+, and jq for the bundled helper script; authoring the JSON payload itself needs no tools. Network access to the regional IndyKite REST API (eu.api.indykite.com or us.api.indykite.com) is required at runtime. --- # IndyKite Capture - delete relationship properties This skill builds the request body for the Capture API's **batch relationship-property delete** endpoint - removing named properties from relationships in the IndyKite Knowledge Graph (IKG) while keeping the relationships themselves: ```text POST /capture/v1/relationships/properties/delete ``` Each entry identifies one relationship (`source`, `target`, `type`) and lists the `property_types` to strip from it. The JSON file is the deliverable, ready to be POSTed by any application. The [MCP server](/agent-skills/indykite-mcp-server/SKILL.md) does not currently expose Capture endpoints; the JSON bodies this skill produces are for direct REST use and remain valid if Capture tools are added later. ## When to use Activate this skill when the user wants to: - **remove properties from an edge** - e.g. drop a stale `status` or timestamp from an `OWNS` relationship - while keeping the connection; - strip the same property set from **many relationships** in one call. Do **not** activate this skill to: - delete the **relationship itself** - use [`indykite-capture-delete-relationships`](/agent-skills/indykite-capture-delete-relationships/SKILL.md); - remove **node** properties - use [`indykite-capture-delete-node-properties`](/agent-skills/indykite-capture-delete-node-properties/SKILL.md); - overwrite a property with a new value - re-upsert the relationship with [`indykite-capture-upsert-relationships`](/agent-skills/indykite-capture-upsert-relationships/SKILL.md); - delete through a **CIQ policy + Knowledge Query** - use [`indykite-ciq-delete`](/agent-skills/indykite-ciq-delete/SKILL.md). ## Prerequisites - An IndyKite **project** with an **AppAgent** whose credentials are configured for the calling application ([Credentials guide](https://developer.indykite.com/guides/guide-credentials)). - For each target: the relationship's `source` and `target` (`type`, `external_id`) pairs, its `type`, and the exact **property names** to remove. ## Steps ### 1. List the relationships and the properties to strip | Field | Required | Meaning | |------------------|----------|--------------------------------------------------------------| | `source` | yes | `{ "external_id": …, "type": … }` of the outgoing node. | | `target` | yes | `{ "external_id": …, "type": … }` of the incoming node. | | `type` | yes | The relationship type (max 128 chars). | | `property_types` | yes | 1-250 property names to delete from this relationship. | ### 2. Assemble the request body One JSON object: `{ "relationships": [ … ] }`, 1-250 entries per request. On a **composite IKG**, add top-level `"use_global_db": true` to target relationships stored in the global constituent ([Data Residency guide](https://developer.indykite.com/guides/guide-data-residency)); omit it on a regular IKG. A ready example: [`assets/delete-relationship-properties.json`](/agent-skills/indykite-capture-delete-relationship-properties/assets/delete-relationship-properties.json). ```json { "relationships": [ { "source": { "external_id": "millicent", "type": "Person" }, "target": { "external_id": "kitt", "type": "Car" }, "type": "OWNS", "property_types": ["status"] } ] } ``` This file is the deliverable - any HTTP-capable application can send it. ### 3. Send it (optional) The endpoint authenticates the **calling application** (its AppAgent credentials). Which credential goes in which request header is covered by the [Credentials guide](https://developer.indykite.com/guides/guide-credentials). A runnable shell helper builds the authenticated request: [`scripts/capture.sh`](/agent-skills/indykite-capture-delete-relationship-properties/scripts/capture.sh) — run with `--print` to preview the `curl` (host-pinned; token redacted). Deletion is destructive: preview with `--print`, and confirm the property list, before sending. ### 4. Read the results A `200` returns one result per entry, in order: `{ "results": [ { "id": "gid:…" } ] }`. Field shapes and error semantics: [`references/capture-reference.md`](/agent-skills/indykite-capture-delete-relationship-properties/references/capture-reference.md). ## Outcome - A valid `{ "relationships": [ … ] }` body exists, each entry identifying a relationship and its `property_types` to remove. - If sent, the listed properties are gone from those relationships; the relationships and their endpoint nodes are untouched. ## Files in this skill - [`references/capture-reference.md`](/agent-skills/indykite-capture-delete-relationship-properties/references/capture-reference.md) - field reference, `use_global_db` behavior, batch limits, and error semantics. - [`assets/delete-relationship-properties.json`](/agent-skills/indykite-capture-delete-relationship-properties/assets/delete-relationship-properties.json) - ready request body stripping `status` from an `OWNS` edge. - [`scripts/capture.sh`](/agent-skills/indykite-capture-delete-relationship-properties/scripts/capture.sh) - Bash helper that POSTs a body file to `/capture/v1/relationships/properties/delete` (host-pinned; `--print` to preview). ## Agent-specific notes This skill uses generic markdown instructions and works across all agents listed in the [README](/agent-skills/README.md). The agent needs to be able to write a JSON file; sending it additionally requires HTTP access (`curl` or any HTTP client). ## References - [Capture API reference (OpenAPI)](https://openapi.indykite.com/) - [Data Residency guide](https://developer.indykite.com/guides/guide-data-residency) - `use_global_db` on composite IKGs - [Credentials guide](https://developer.indykite.com/guides/guide-credentials) --- --- name: indykite-capture-delete-relationships description: Build the request-body JSON for the IndyKite Capture API batch relationship delete (`POST /capture/v1/relationships/delete`) - a `relationships` array (1-250 per request), each entry identifying a relationship by `source` node, `target` node (each `external_id` + `type`), and relationship `type`; on composite IKGs setting the top-level `use_global_db` field to `true` targets relationships stored in the global constituent. Use when the user wants to disconnect entities in the IndyKite Knowledge Graph (IKG) - "remove the CAN_DRIVE link between ryan and kitt", "unlink these contracts from their vehicles", "prepare a relationship-delete payload for the Capture API". Produces a ready-to-send JSON file; sending it is optional. The endpoint nodes survive. Not for deleting nodes (indykite-capture-delete-nodes), removing only relationship properties (indykite-capture-delete-relationship-properties), creating relationships (indykite-capture-upsert-relationships), or CIQ policy-mediated deletes (indykite-ciq-delete). license: Apache-2.0 compatibility: Requires curl, bash 4+, and jq for the bundled helper script; authoring the JSON payload itself needs no tools. Network access to the regional IndyKite REST API (eu.api.indykite.com or us.api.indykite.com) is required at runtime. --- # IndyKite Capture - delete relationships This skill builds the request body for the Capture API's **batch relationship delete** endpoint - removing typed connections between nodes in the IndyKite Knowledge Graph (IKG) while leaving the nodes in place: ```text POST /capture/v1/relationships/delete ``` Each entry identifies one relationship the same way it was created: `source` node, `target` node, and relationship `type`. The JSON file is the deliverable, ready to be POSTed by any application. The [MCP server](/agent-skills/indykite-mcp-server/SKILL.md) does not currently expose Capture endpoints; the JSON bodies this skill produces are for direct REST use and remain valid if Capture tools are added later. ## When to use Activate this skill when the user wants to: - **disconnect** two entities - revoke an `OWNS` / `CAN_DRIVE` / `ACCEPTED` edge - while keeping both nodes; - undo a batch created with [`indykite-capture-upsert-relationships`](/agent-skills/indykite-capture-upsert-relationships/SKILL.md). Do **not** activate this skill to: - delete the **nodes** themselves - use [`indykite-capture-delete-nodes`](/agent-skills/indykite-capture-delete-nodes/SKILL.md); - remove only a relationship's **properties** (edge survives) - use [`indykite-capture-delete-relationship-properties`](/agent-skills/indykite-capture-delete-relationship-properties/SKILL.md); - **create** relationships - use [`indykite-capture-upsert-relationships`](/agent-skills/indykite-capture-upsert-relationships/SKILL.md); - delete through a **CIQ policy + Knowledge Query** - use [`indykite-ciq-delete`](/agent-skills/indykite-ciq-delete/SKILL.md). ## Prerequisites - An IndyKite **project** with an **AppAgent** whose credentials are configured for the calling application ([Credentials guide](https://developer.indykite.com/guides/guide-credentials)). - For each relationship: the `source` and `target` (`type`, `external_id`) pairs and the relationship `type`. ## Steps ### 1. List the relationships to delete | Field | Required | Meaning | |----------|----------|--------------------------------------------------------------| | `source` | yes | `{ "external_id": …, "type": … }` of the outgoing node. | | `target` | yes | `{ "external_id": …, "type": … }` of the incoming node. | | `type` | yes | The relationship type to remove between them (max 128 chars). | ### 2. Assemble the request body One JSON object: `{ "relationships": [ … ] }`, 1-250 entries per request. On a **composite IKG**, add top-level `"use_global_db": true` to delete relationships stored in the global constituent ([Data Residency guide](https://developer.indykite.com/guides/guide-data-residency)); omit it on a regular IKG. A ready example: [`assets/delete-relationships.json`](/agent-skills/indykite-capture-delete-relationships/assets/delete-relationships.json). ```json { "relationships": [ { "source": { "external_id": "ryan", "type": "Person" }, "target": { "external_id": "kitt", "type": "Car" }, "type": "CAN_DRIVE" } ] } ``` This file is the deliverable - any HTTP-capable application can send it. ### 3. Send it (optional) The endpoint authenticates the **calling application** (its AppAgent credentials). Which credential goes in which request header is covered by the [Credentials guide](https://developer.indykite.com/guides/guide-credentials). A runnable shell helper builds the authenticated request: [`scripts/capture.sh`](/agent-skills/indykite-capture-delete-relationships/scripts/capture.sh) — run with `--print` to preview the `curl` (host-pinned; token redacted). Deletion is destructive: preview with `--print`, and confirm the edge list, before sending. ### 4. Read the results A `200` returns one result per relationship, in order: `{ "results": [ { "id": "gid:…" } ] }`. Field shapes and error semantics: [`references/capture-reference.md`](/agent-skills/indykite-capture-delete-relationships/references/capture-reference.md). ## Outcome - A valid `{ "relationships": [ … ] }` delete body exists, each entry naming `source`, `target`, and `type`. - If sent, those relationships are gone from the IKG - policy conditions and queries that traversed them no longer match - while both endpoint nodes remain. ## Files in this skill - [`references/capture-reference.md`](/agent-skills/indykite-capture-delete-relationships/references/capture-reference.md) - field reference, `use_global_db` behavior, batch limits, and error semantics. - [`assets/delete-relationships.json`](/agent-skills/indykite-capture-delete-relationships/assets/delete-relationships.json) - ready request body removing a `CAN_DRIVE` edge. - [`scripts/capture.sh`](/agent-skills/indykite-capture-delete-relationships/scripts/capture.sh) - Bash helper that POSTs a body file to `/capture/v1/relationships/delete` (host-pinned; `--print` to preview). ## Agent-specific notes This skill uses generic markdown instructions and works across all agents listed in the [README](/agent-skills/README.md). The agent needs to be able to write a JSON file; sending it additionally requires HTTP access (`curl` or any HTTP client). ## References - [Capture API reference (OpenAPI)](https://openapi.indykite.com/) - [Data Residency guide](https://developer.indykite.com/guides/guide-data-residency) - `use_global_db` on composite IKGs - [Credentials guide](https://developer.indykite.com/guides/guide-credentials) --- --- name: indykite-capture-upsert-nodes description: Build the request-body JSON for the IndyKite Capture API batch node upsert (`POST /capture/v1/nodes`) - a `nodes` array (1-250 per request) of entities, each with `external_id`, `type`, optional `is_identity` / `labels` / `location`, and typed `properties` (values, external-data references, and per-property metadata such as source, assurance level, and verified time). Use when the user wants to ingest or update entities in the IndyKite Knowledge Graph (IKG) - "add these people and cars to the graph", "upsert this customer with verified email metadata", "prepare a nodes payload for the Capture API". Produces a ready-to-send JSON file; sending it is optional. Not for connecting nodes (indykite-capture-upsert-relationships), removing them (indykite-capture-delete-nodes), or CIQ policy-mediated writes (indykite-ciq-create-node). license: Apache-2.0 compatibility: Requires curl, bash 4+, and jq for the bundled helper script; authoring the JSON payload itself needs no tools. Network access to the regional IndyKite REST API (eu.api.indykite.com or us.api.indykite.com) is required at runtime. --- # IndyKite Capture - upsert nodes The Capture API is the direct ingestion surface of the IndyKite Knowledge Graph (IKG): it writes entities (**nodes**) and connections (**relationships**) without any policy in between. This skill builds the request body for the **batch node upsert** endpoint - the JSON file is the deliverable, ready to be POSTed by any application, CI job, or shell helper: ```text POST /capture/v1/nodes ``` Upsert semantics: a node is identified by its (`type`, `external_id`) pair - posting the same pair again **updates** the node instead of creating a duplicate. The [MCP server](/agent-skills/indykite-mcp-server/SKILL.md) does not currently expose Capture endpoints (its tools cover AuthZEN decisions and CIQ queries); the JSON bodies this skill produces are for direct REST use and remain valid if Capture tools are added later. ## When to use Activate this skill when the user wants to: - **ingest entities** into the IKG - people, organizations, devices, resources - as graph nodes; - **update** an existing node's properties (same `type` + `external_id`); - attach **property metadata** (source, assurance level, verified time) or an **external-data reference** (`external_value`) to a property; - prepare graph data that [`indykite-authzen-*`](/agent-skills/indykite-authzen-kbac-policies/SKILL.md) policies or [`indykite-ciq-*`](/agent-skills/indykite-ciq-read/SKILL.md) queries will later match. Do **not** activate this skill to: - **connect** nodes - use [`indykite-capture-upsert-relationships`](/agent-skills/indykite-capture-upsert-relationships/SKILL.md); - **remove** nodes, properties, or metadata - use the [`indykite-capture-delete-*`](/agent-skills/indykite-capture-delete-nodes/SKILL.md) skills; - write through a **CIQ policy + Knowledge Query** (parameterized, authorization-gated writes) - use [`indykite-ciq-create-node`](/agent-skills/indykite-ciq-create-node/SKILL.md); the Capture API writes directly, with no policy involved; - **read** graph data - use [`indykite-ciq-read`](/agent-skills/indykite-ciq-read/SKILL.md). ## Prerequisites - An IndyKite **project** with an **AppAgent** whose credentials are configured for the calling application ([Credentials guide](https://developer.indykite.com/guides/guide-credentials)). - The **graph model**: node types, their identifying `external_id` scheme, and the property names downstream queries and policies will reference. ## Steps ### 1. Model the nodes For each entity decide: | Field | Required | Meaning | |---------------|----------|--------------------------------------------------------------------------------------------| | `external_id` | yes | Your identifier for the node (1-256 chars). Upserts key on (`type`, `external_id`). | | `type` | yes | Node type / label, e.g. `Person`, `Car` (2-64 chars). | | `is_identity` | no | `true` marks an identity node (a person or other actor, e.g. an AuthZEN subject); omit or `false` for plain entities. | | `labels` | no | Extra labels beyond `type`. | | `location` | no | Composite-IKG routing only: a logical location (an `alias_mapping` key, 2-32 chars). Omit on a regular IKG. | | `properties` | no | Array of typed values - see step 2. | ### 2. Write the properties Each property is `{ "type": ..., "value": ... }`; `value` is a string, integer, float, boolean, or an array of those. Two optional extensions: - **`metadata`** - provenance for the single property: `source` (string), `assurance_level` (1, 2, or 3), `verified_time` (RFC 3339 timestamp), `custom_metadata` (object). Used e.g. by [trust scoring](https://developer.indykite.com/guides/guide-trust-score). - **`external_value`** - instead of `value`, a data reference resolved at query time by an [External Data Resolver](https://developer.indykite.com/guides/guide-external-data-resolver), so the actual data never lives in the IKG. ### 3. Assemble the request body The body is one JSON object: `{ "nodes": [ … ] }` with 1-250 nodes per request (batch larger sets into multiple files). A ready example - two `Person` identities and a `Car`, one property carrying metadata: [`assets/nodes-vehicle-rental.json`](/agent-skills/indykite-capture-upsert-nodes/assets/nodes-vehicle-rental.json). ```json { "nodes": [ { "external_id": "millicent", "type": "Person", "is_identity": true, "properties": [ { "type": "email", "value": "millicent@email.com" }, { "type": "name", "value": "Millicent Contextsworth", "metadata": { "assurance_level": 1, "source": "Some Source", "verified_time": "2026-04-10T06:28:16Z" } } ] }, { "external_id": "kitt", "type": "Car", "properties": [ { "type": "manufacturer", "value": "pontiac" } ] } ] } ``` This file is the deliverable - any HTTP-capable application can send it. ### 4. Send it (optional) The endpoint authenticates the **calling application** (its AppAgent credentials). Which credential goes in which request header is covered by the [Credentials guide](https://developer.indykite.com/guides/guide-credentials). A runnable shell helper builds the authenticated request: [`scripts/capture.sh`](/agent-skills/indykite-capture-upsert-nodes/scripts/capture.sh) — run with `--print` to preview the `curl` (host-pinned; token redacted). ### 5. Read the results A `200` returns one result per node, in order: ```json { "results": [ { "id": "gid:…" }, { "id": "gid:…" } ] } ``` `400` (with `errors[]`) means a malformed body - commonly a missing `external_id` or `type`. Field shapes, batch limits, and error semantics: [`references/capture-reference.md`](/agent-skills/indykite-capture-upsert-nodes/references/capture-reference.md). To verify the ingest at schema level (types, property names, counts), read the schema back with [`indykite-data-schema`](/agent-skills/indykite-data-schema/SKILL.md). ## Outcome - A valid `{ "nodes": [ … ] }` JSON file exists, each node carrying `external_id`, `type`, and its properties. - If sent, the nodes exist in the IKG (created or updated by `external_id`), visible to CIQ queries, KBAC decisions, and the Hub Explorer. ## Files in this skill - [`references/capture-reference.md`](/agent-skills/indykite-capture-upsert-nodes/references/capture-reference.md) - full field reference (node, property, metadata, external_value, location), batch limits, and error semantics. - [`assets/nodes-vehicle-rental.json`](/agent-skills/indykite-capture-upsert-nodes/assets/nodes-vehicle-rental.json) - ready request body: two `Person` identities and a `Car` with property metadata. - [`scripts/capture.sh`](/agent-skills/indykite-capture-upsert-nodes/scripts/capture.sh) - Bash helper that POSTs a body file to `/capture/v1/nodes` (host-pinned; `--print` to preview). ## Agent-specific notes This skill uses generic markdown instructions and works across all agents listed in the [README](/agent-skills/README.md). The agent needs to be able to write a JSON file; sending it additionally requires HTTP access (`curl` or any HTTP client). ## References - [Capture API reference (OpenAPI)](https://openapi.indykite.com/) - [Ingest data into the IKG (developer hub resource)](https://developer.indykite.com/resources/capture-1) - [Data Residency guide](https://developer.indykite.com/guides/guide-data-residency) - `location` routing on composite IKGs - [External Data Resolver guide](https://developer.indykite.com/guides/guide-external-data-resolver) - `external_value` data references - [Trust Score guide](https://developer.indykite.com/guides/guide-trust-score) - how property metadata feeds scoring - [Credentials guide](https://developer.indykite.com/guides/guide-credentials) --- --- name: indykite-capture-upsert-relationships description: Build the request-body JSON for the IndyKite Capture API batch relationship upsert (`POST /capture/v1/relationships`) - a `relationships` array (1-250 per request), each entry connecting a `source` node to a `target` node (by `external_id` + `type`) with a relationship `type` and optional typed `properties`; on composite IKGs setting `use_global_db` to `true` routes cross-location relationships to the global constituent. Use when the user wants to connect existing entities in the IndyKite Knowledge Graph (IKG) - "link millicent OWNS kitt", "wire these contracts to their vehicles", "prepare a relationships payload for the Capture API". Produces a ready-to-send JSON file; sending it is optional. Not for creating the nodes themselves (indykite-capture-upsert-nodes), removing relationships (indykite-capture-delete-relationships), or CIQ policy-mediated writes (indykite-ciq-create-relationship). license: Apache-2.0 compatibility: Requires curl, bash 4+, and jq for the bundled helper script; authoring the JSON payload itself needs no tools. Network access to the regional IndyKite REST API (eu.api.indykite.com or us.api.indykite.com) is required at runtime. --- # IndyKite Capture - upsert relationships The Capture API is the direct ingestion surface of the IndyKite Knowledge Graph (IKG). This skill builds the request body for the **batch relationship upsert** endpoint - connecting nodes that already exist (or are being ingested alongside, see [`indykite-capture-upsert-nodes`](/agent-skills/indykite-capture-upsert-nodes/SKILL.md)): ```text POST /capture/v1/relationships ``` Each entry names a `source` node, a `target` node (each by `external_id` + `type`), and the relationship `type` - e.g. `Person(millicent) -[OWNS]-> Car(kitt)`. The JSON file is the deliverable, ready to be POSTed by any application. The [MCP server](/agent-skills/indykite-mcp-server/SKILL.md) does not currently expose Capture endpoints; the JSON bodies this skill produces are for direct REST use and remain valid if Capture tools are added later. ## When to use Activate this skill when the user wants to: - **connect** two entities in the IKG with a typed relationship (`OWNS`, `ACCEPTED`, `COVERS`, `HAS`, …); - attach **properties** to a relationship (e.g. a `status` or a timestamp); - build the relationship structure that [`indykite-authzen-*`](/agent-skills/indykite-authzen-kbac-policies/SKILL.md) policy conditions or [`indykite-ciq-*`](/agent-skills/indykite-ciq-read/SKILL.md) queries traverse. Do **not** activate this skill to: - **create the nodes** being connected - use [`indykite-capture-upsert-nodes`](/agent-skills/indykite-capture-upsert-nodes/SKILL.md); - **remove** relationships or their properties - use [`indykite-capture-delete-relationships`](/agent-skills/indykite-capture-delete-relationships/SKILL.md) / [`indykite-capture-delete-relationship-properties`](/agent-skills/indykite-capture-delete-relationship-properties/SKILL.md); - write through a **CIQ policy + Knowledge Query** - use [`indykite-ciq-create-relationship`](/agent-skills/indykite-ciq-create-relationship/SKILL.md); the Capture API writes directly, with no policy involved. ## Prerequisites - An IndyKite **project** with an **AppAgent** whose credentials are configured for the calling application ([Credentials guide](https://developer.indykite.com/guides/guide-credentials)). - The **endpoint nodes**: each `source`/`target` is referenced by (`type`, `external_id`) - ingest them first (or in the same session) with [`indykite-capture-upsert-nodes`](/agent-skills/indykite-capture-upsert-nodes/SKILL.md). ## Steps ### 1. Model the relationships For each connection decide: | Field | Required | Meaning | |--------------|----------|---------------------------------------------------------------------| | `source` | yes | `{ "external_id": …, "type": … }` of the outgoing node. | | `target` | yes | `{ "external_id": …, "type": … }` of the incoming node. | | `type` | yes | Relationship type, conventionally an uppercase verb (max 128 chars). | | `properties` | no | Array of `{ "type": …, "value": … }` (string / integer / float / boolean or arrays of those); `external_value` data references are also accepted. | ### 2. Assemble the request body One JSON object: `{ "relationships": [ … ] }`, 1-250 entries per request. On a **composite IKG**, add top-level `"use_global_db": true` - relationships can connect nodes living in different locations, so they are stored in the global constituent alongside the proxy nodes ([Data Residency guide](https://developer.indykite.com/guides/guide-data-residency)). Omit it on a regular IKG. A ready example: [`assets/relationships-vehicle-rental.json`](/agent-skills/indykite-capture-upsert-relationships/assets/relationships-vehicle-rental.json). ```json { "relationships": [ { "source": { "external_id": "millicent", "type": "Person" }, "target": { "external_id": "kitt", "type": "Car" }, "type": "OWNS", "properties": [ { "type": "status", "value": "active" } ] } ] } ``` ### 3. Send it (optional) The endpoint authenticates the **calling application** (its AppAgent credentials). Which credential goes in which request header is covered by the [Credentials guide](https://developer.indykite.com/guides/guide-credentials). A runnable shell helper builds the authenticated request: [`scripts/capture.sh`](/agent-skills/indykite-capture-upsert-relationships/scripts/capture.sh) — run with `--print` to preview the `curl` (host-pinned; token redacted). ### 4. Read the results A `200` returns one result per relationship, in order: `{ "results": [ { "id": "gid:…" } ] }`. Field shapes and error semantics: [`references/capture-reference.md`](/agent-skills/indykite-capture-upsert-relationships/references/capture-reference.md). ## Outcome - A valid `{ "relationships": [ … ] }` JSON file exists, each entry naming `source`, `target`, and `type`. - If sent, the relationships exist in the IKG between the referenced nodes, traversable by CIQ queries and KBAC policy conditions. ## Files in this skill - [`references/capture-reference.md`](/agent-skills/indykite-capture-upsert-relationships/references/capture-reference.md) - full field reference (relationship, node reference, properties, `use_global_db`), batch limits, and error semantics. - [`assets/relationships-vehicle-rental.json`](/agent-skills/indykite-capture-upsert-relationships/assets/relationships-vehicle-rental.json) - ready request body: `OWNS` (with a property) and `CAN_DRIVE` relationships. - [`scripts/capture.sh`](/agent-skills/indykite-capture-upsert-relationships/scripts/capture.sh) - Bash helper that POSTs a body file to `/capture/v1/relationships` (host-pinned; `--print` to preview). ## Agent-specific notes This skill uses generic markdown instructions and works across all agents listed in the [README](/agent-skills/README.md). The agent needs to be able to write a JSON file; sending it additionally requires HTTP access (`curl` or any HTTP client). ## References - [Capture API reference (OpenAPI)](https://openapi.indykite.com/) - [Ingest data into the IKG (developer hub resource)](https://developer.indykite.com/resources/capture-1) - [Data Residency guide](https://developer.indykite.com/guides/guide-data-residency) - `use_global_db` on composite IKGs - [Credentials guide](https://developer.indykite.com/guides/guide-credentials) --- --- name: indykite-ciq-add-property description: Author an IndyKite ContX IQ (CIQ) policy plus its Knowledge Query that sets one or more properties on an existing node 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 - no node creation, no relationship 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 node Set or overwrite one or more properties on a node 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 nodes may be modified (`allowed_upserts.nodes.existing_nodes`); the Knowledge Query's `upsert_nodes` references those variables (no `external_id`, since the node already exists) and lists the properties to set, optionally with metadata. The IKG treats this as an upsert - adding a brand-new property and overwriting an existing one are the **same operation**; the platform doesn't distinguish. This skill covers exactly that - property writes on an existing node. Other paths are deliberately out of scope: - **Creating a brand-new node** uses `allowed_upserts.nodes.node_types` and a Knowledge Query `upsert_nodes` entry with a fresh `name` + an `external_id` - see [`indykite-ciq-create-node`](/agent-skills/indykite-ciq-create-node/SKILL.md). - **Creating a new relationship** uses `allowed_upserts.relationships.relationship_types` - see [`indykite-ciq-create-relationship`](/agent-skills/indykite-ciq-create-relationship/SKILL.md). - **Updating a relationship's properties** uses `allowed_upserts.relationships.existing_relationships`. - **Deleting a property** uses `allowed_deletes.nodes` with a `.property.` path. For reads, see [`indykite-ciq-read`](/agent-skills/indykite-ciq-read/SKILL.md). ## When to use Activate this skill when the user: - wants to **set a property** on a node that already exists in the IKG (e.g. update a Person's `music_mood`, set a LicenseNumber's `status`, attach `assurance_level` metadata to a verified property); - is authoring a `Person`-subject "update own data" policy (the canonical pattern: `MATCH (subject:Person)` + `subject.external_id = $token.sub` + `existing_nodes: ["subject"]`); - is authoring an `_Application`-subject "system-side property write" policy that updates a node reachable from the AppAgent; - needs to write **property metadata** (`source`, `assurance_level`, custom metadata fields); - is debugging a `403` / `422` from a property-write execute that should have succeeded. Do **not** activate this skill when the user: - wants to **create a brand-new node** - use [`indykite-ciq-create-node`](/agent-skills/indykite-ciq-create-node/SKILL.md); - wants to **link two existing nodes** with a new relationship - use [`indykite-ciq-create-relationship`](/agent-skills/indykite-ciq-create-relationship/SKILL.md); - wants to **update a relationship's properties** - different policy field (`existing_relationships`), out of scope here; - wants to **delete** a property or node - different policy field (`allowed_deletes`); - wants to **read** data - use [`indykite-ciq-read`](/agent-skills/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 node already in the IKG** - CIQ doesn't seed it; this policy authorises modifying its properties. - 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 `. | `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 `Person` ("update own profile"); an `_Application` variant - for example, an ETL job that backfills `imported_at` timestamps - differs only in `subject.type`, the filter, and the execute headers. **Cypher pattern** - the `MATCH` clause that **resolves the node you intend to update**. The variable name you use here is what `existing_nodes` and `upsert_nodes[].name` will reference. The simplest case is `MATCH (subject:Person)` (the subject node itself); the more general case walks a path to a related node, e.g. `MATCH (subject:Person)-[:OWNS]->(car:Car)-[:HAS]->(ln:LicenseNumber)`. 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`](/agent-skills/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, modelled on the music-dataset Chapter 8 `ciqpolicy4`): > A `Person` updates their own profile properties (e.g. `music_mood`, `dance_skill`). ```cypher MATCH (subject:Person) ``` Variable: `subject`. The KQ will reference this name in `upsert_nodes`. ### 2. Author the policy with `allowed_upserts.nodes.existing_nodes` Build the policy JSON with four blocks: - `meta.policy_version` - currently `1.0-ciq`. - `subject.type` - `Person` for the running example. - `condition.cypher` and `condition.filter` - anchor the node to update. For `Person`, filter on `subject.external_id = $token.sub`. - `allowed_upserts.nodes.existing_nodes` - array of variables from `cypher` whose properties the Knowledge Query may write. The Knowledge Query's `upsert_nodes[].name` must be in this list. **Omit** `allowed_reads`, `allowed_deletes`, and the other `allowed_upserts` sub-fields if this policy only writes properties. Combining with `allowed_reads` is common in practice (read-and-update-own-profile patterns) but kept out of scope here for clarity. A complete write-only policy for the running example: see [`assets/policy-update-own-profile.json`](/agent-skills/indykite-ciq-add-property/assets/policy-update-own-profile.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-property/assets/policy-update-own-profile.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 `node_types`, the `_Application` variant, the metadata variant from `policyMetaData`) see [`references/policy-reference.md`](/agent-skills/indykite-ciq-add-property/references/policy-reference.md). ### 3. Create the Knowledge Query with `upsert_nodes` The Knowledge Query references the policy. Each entry in `upsert_nodes` describes one node-update: - `name` - **must** match a variable from the policy's `cypher` (e.g. `subject`, `car`, `ln`). This is what differs structurally from the create-node skill; using a fresh name here would imply create. - `type` - *omit* when updating an existing node. (For creates it would specify the new node's label; for updates the label is whatever the matched node already has.) - `external_id` - **omit**. Required only for creates. - `properties` - array of `{type, value, metadata?}` items. `type` (property name) is hardcoded; `value` may be hardcoded or `$param`; `metadata` is optional and follows the same rules. Echo the result back in the response by listing properties to project in the top-level `nodes` array, e.g. `subject.property.music_mood`. This confirms the value that was written. A complete Knowledge Query for the running example: see [`assets/knowledge-query-update-own-profile.json`](/agent-skills/indykite-ciq-add-property/assets/knowledge-query-update-own-profile.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-property/assets/knowledge-query-update-own-profile.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`, `external_id`, `id`, `type`, `update_time`), the metadata sub-array, and the rich `knowledgeQueryMetaData` example with `$token.iss` substitution - live in [`references/knowledge-query-reference.md`](/agent-skills/indykite-ciq-add-property/references/knowledge-query-reference.md). ### 4. Authenticate and execute The execute endpoint is the same as for reads, node-creates, and relationship-creates: ```text POST /contx-iq/v1/execute ``` Authentication for the running `Person`-subject example: - `X-IK-ClientKey: ` - required. - `Authorization: Bearer ` - required. The token's `sub` claim drives `$token.sub` in the policy filter, pinning the cypher anchor to that one user. For `_Application`-subject property writes, omit the Bearer header; the reserved `$_appId` is auto-filled from the AppAgent. Request: ```json { "id": "", "input_params": { "new_music_mood": "Acoustic Sadness", "new_dance_skill": 0.67 } } ``` A runnable shell helper: [`scripts/execute.sh`](/agent-skills/indykite-ciq-add-property/scripts/execute.sh). Full execute reference (auth combinations, response shape, error semantics): [`references/execution-reference.md`](/agent-skills/indykite-ciq-add-property/references/execution-reference.md). ### 5. Verify the response and confirm the property write A successful property-write execute echoes the projection you listed in the KQ's `nodes` array: ```json { "data": [ { "nodes": { "subject.property.music_mood": "Acoustic Sadness", "subject.property.dance_skill": 0.67 } } ] } ``` If the response is **not** what you expected, walk this list before changing the policy or KQ: 1. **Variable in `existing_nodes`.** The KQ's `upsert_nodes[].name` must be in the policy's `allowed_upserts.nodes.existing_nodes`. Mismatch → `403`. 2. **Cypher matched a node.** If the cypher returns no rows (e.g. the user's `external_id` isn't seeded as a Person), the upsert has nothing to attach to - `200` with empty `data`. 3. **`name` matches a cypher variable.** Using a fresh name (one not in cypher) makes the platform interpret the entry as a create - usually rejected because the matching `node_types` whitelist isn't there. 4. **No `external_id` in the `upsert_nodes` entry.** Including `external_id` flips the operation to "create" semantics. For property writes on an existing match, omit it. 5. **Property names not protected.** `_service`, `create_time`, `external_id`, `id`, `type`, `update_time` cannot be set as properties - they're managed by the platform. 6. **Property value type matches the IKG schema.** Sending `"-7.5"` (string) for a numeric property is rejected. For other failure modes (auth shape wrong, missing input_params, metadata weirdness) see [`references/troubleshooting.md`](/agent-skills/indykite-ciq-add-property/references/troubleshooting.md). ## Outcome When this skill has been applied successfully: - A property-write CIQ policy exists; it has a single `subject.type`, a Cypher pattern that resolves to the node to update, optional partial filters, and an `allowed_upserts.nodes.existing_nodes` whitelist - no `node_types`, no `allowed_reads`, no `allowed_deletes`. - A Knowledge Query references that policy and lists `upsert_nodes` entries that reuse cypher variable names, omit `external_id`, and declare the properties (and optional metadata) to set. - `POST /contx-iq/v1/execute` returns the projected property values, confirming the write. - A follow-up read query (e.g. via [`indykite-ciq-read`](/agent-skills/indykite-ciq-read/SKILL.md)) finds the new property values on the node. ## Files in this skill - [`references/policy-reference.md`](/agent-skills/indykite-ciq-add-property/references/policy-reference.md) - write-focused policy schema, `existing_nodes` deep-dive, the Person and `_Application` patterns, why other blocks are omitted. - [`references/knowledge-query-reference.md`](/agent-skills/indykite-ciq-add-property/references/knowledge-query-reference.md) - `upsert_nodes` for updates (variable from cypher, no `external_id`), properties + metadata, the `knowledgeQueryMetaData` rich example, protected property names. - [`references/execution-reference.md`](/agent-skills/indykite-ciq-add-property/references/execution-reference.md) - `POST /contx-iq/v1/execute` for property writes, auth combinations, response shape including the rich `Props` block. - [`references/troubleshooting.md`](/agent-skills/indykite-ciq-add-property/references/troubleshooting.md) - `403` / empty-`data` / type-mismatch / metadata patterns. - [`assets/policy-update-own-profile.json`](/agent-skills/indykite-ciq-add-property/assets/policy-update-own-profile.json) - runnable Person-subject "update own profile" policy, modelled on music-dataset Chapter 8 `ciqpolicy4`. - [`assets/knowledge-query-update-own-profile.json`](/agent-skills/indykite-ciq-add-property/assets/knowledge-query-update-own-profile.json) - matching Knowledge Query (sets `music_mood` and `dance_skill`). - [`scripts/execute.sh`](/agent-skills/indykite-ciq-add-property/scripts/execute.sh) - Bash helper that posts to `/contx-iq/v1/execute` with the right headers. ## Agent-specific notes This skill uses generic markdown instructions and works across all agents listed in the [README](/agent-skills/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.nodes.existing_nodes` and the `properties` / `metadata` arrays. - [Music dataset tutorial - Chapter 8 "ContX IQ policies"](https://developer.indykite.com/tutorials/tutorial-music-dataset) - `ciqpolicy4` is the canonical Person-subject "update own data" pattern; `kq4b` is its write variant. - [Music dataset tutorial - Chapter 9 "Knowledge Queries"](https://developer.indykite.com/tutorials/tutorial-music-dataset) - read/write/delete variant naming convention (`kq` / `kqb` / `kqc`). - [Developer-hub resources - CIQ examples](https://developer.indykite.com/resources) - `policyMetaData` + `knowledgeQueryMetaData` show a richer property-write pattern with `$token.iss` substitution and per-property metadata. - [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) - [Credentials guide](https://developer.indykite.com/guides/guide-credentials) --- --- 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`](/agent-skills/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`](/agent-skills/indykite-ciq-create-relationship/SKILL.md). - **Setting properties on a node** uses `allowed_upserts.nodes.existing_nodes` - see [`indykite-ciq-add-property`](/agent-skills/indykite-ciq-add-property/SKILL.md). - **Deleting a property on a relationship** uses `allowed_deletes.relationships` with a `.` path - see [`indykite-ciq-delete`](/agent-skills/indykite-ciq-delete/SKILL.md). For reads, see [`indykite-ciq-read`](/agent-skills/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`](/agent-skills/indykite-ciq-create-relationship/SKILL.md); - wants to **set properties on a node** - use [`indykite-ciq-add-property`](/agent-skills/indykite-ciq-add-property/SKILL.md); - wants to **delete** a property - use [`indykite-ciq-delete`](/agent-skills/indykite-ciq-delete/SKILL.md); - wants to **read** data - use [`indykite-ciq-read`](/agent-skills/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 `. | `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`](/agent-skills/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`](/agent-skills/indykite-ciq-add-relationship-property/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`](/agent-skills/indykite-ciq-add-relationship-property/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`](/agent-skills/indykite-ciq-add-relationship-property/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`](/agent-skills/indykite-ciq-add-relationship-property/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 /contx-iq/v1/execute ``` Authentication for the running `_Application`-subject example: - `X-IK-ClientKey: ` - required. - `Authorization: Bearer …` - **omit** for `_Application`. For Person-subject relationship-property writes, add `Authorization: Bearer `. Request: ```json { "id": "", "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`](/agent-skills/indykite-ciq-add-relationship-property/scripts/execute.sh). Full execute reference: [`references/execution-reference.md`](/agent-skills/indykite-ciq-add-relationship-property/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`](/agent-skills/indykite-ciq-add-relationship-property/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`](/agent-skills/indykite-ciq-read/SKILL.md)) finds the new property values on the relationship. ## Files in this skill - [`references/policy-reference.md`](/agent-skills/indykite-ciq-add-relationship-property/references/policy-reference.md) - policy schema, `existing_relationships` deep-dive, why other blocks are omitted. - [`references/knowledge-query-reference.md`](/agent-skills/indykite-ciq-add-relationship-property/references/knowledge-query-reference.md) - `upsert_relationships` for updates (variable from cypher, no `source`/`target`/`type`), properties + metadata, protected names. - [`references/execution-reference.md`](/agent-skills/indykite-ciq-add-relationship-property/references/execution-reference.md) - `POST /contx-iq/v1/execute` for relationship-property writes, response shape with the relationship's `Props` block. - [`references/troubleshooting.md`](/agent-skills/indykite-ciq-add-relationship-property/references/troubleshooting.md) - symptom → fix tables. - [`assets/policy-annotate-played-at.json`](/agent-skills/indykite-ciq-add-relationship-property/assets/policy-annotate-played-at.json) - runnable `_Application` annotates `:PLAYED_AT` policy. - [`assets/knowledge-query-annotate-played-at.json`](/agent-skills/indykite-ciq-add-relationship-property/assets/knowledge-query-annotate-played-at.json) - matching Knowledge Query. - [`scripts/execute.sh`](/agent-skills/indykite-ciq-add-relationship-property/scripts/execute.sh) - Bash helper. ## Agent-specific notes This skill uses generic markdown instructions and works across all agents listed in the [README](/agent-skills/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) --- --- name: indykite-ciq-create-node-with-link description: Author an IndyKite ContX IQ (CIQ) policy plus its Knowledge Query that creates a brand-new node AND links it to one or more existing nodes via new relationships in a single `POST /contx-iq/v1/execute` call. Use when ingesting a new entity that must be wired into the IKG atomically - combines node creation and relationship creation in one operation. 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 - create a new node + link it to existing nodes Create a brand-new node in the IndyKite Graph (IKG) and wire it to one or more existing nodes in a single atomic `POST /contx-iq/v1/execute` call. The policy whitelists both a node label and one or more relationship triples, and the Knowledge Query carries both `upsert_nodes` (for the new node) and `upsert_relationships` (for the new edge(s)); the new node's variable `name` from `upsert_nodes` is referenced as the `source` or `target` in `upsert_relationships`. It combines the patterns from [`indykite-ciq-create-node`](/agent-skills/indykite-ciq-create-node/SKILL.md) and [`indykite-ciq-create-relationship`](/agent-skills/indykite-ciq-create-relationship/SKILL.md). This is the **canonical "ingest a new entity into the graph" pattern** - used in the IndyKite developer-hub resources for the insurance Contract example (`policyAllowWriteContract` + `knowledgeQueryAllowWriteContract`), where one execute creates a new `Contract` node and wires it via two relationships (`COVERS` to a Vehicle, `ACCEPTED` from a Person). Other paths are deliberately out of scope: - **Just creating a node, no link** - use [`indykite-ciq-create-node`](/agent-skills/indykite-ciq-create-node/SKILL.md). - **Just linking two existing nodes** - use [`indykite-ciq-create-relationship`](/agent-skills/indykite-ciq-create-relationship/SKILL.md). - **Updating an existing node's properties or relationship's properties** - different operations entirely. ## When to use Activate this skill when the user: - wants to **ingest a new entity** through CIQ in one atomic operation (create the node *and* its relationships to existing nodes); - is implementing the canonical insurance/contract pattern: a new `Contract` node linked to an existing `Vehicle` and an existing `Person`; - is building an "add a comment to a document" flow: a new `Comment` node linked to an existing `Document`; - is parameterising both the new node's `external_id` and the source/target endpoints from `input_params`; - is debugging a `403` / `422` from a combined create execute that should have wired the new node up. Do **not** activate this skill when the user: - only needs to **create a node** - use [`indykite-ciq-create-node`](/agent-skills/indykite-ciq-create-node/SKILL.md); - only needs to **link two existing nodes** - use [`indykite-ciq-create-relationship`](/agent-skills/indykite-ciq-create-relationship/SKILL.md); - needs to write properties on existing elements - use the property-write skills. ## Prerequisites - An IndyKite **project**, **AppAgent**, and AppAgent **credentials**. - 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 **endpoint nodes** the new node will link to **already in the IKG**. - The **node label and relationship label(s)** the operation will use, allowed by the project's data model. - A **plan for the new node's `external_id`** - usually parameterised via `$param`. ## Steps ### 1. Pick the subject and the cypher pattern **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 `. | `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` (insurance-contract ingestion); a `Person` variant - for example, a user posting a new `Comment` linked to an existing `Document` they own - differs only in `subject.type`, the filter, and the execute headers. **Cypher pattern** - must `MATCH` the subject **and** every existing endpoint the new node will link to. The new node itself is **not** matched; it's declared in `upsert_nodes`. 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`](/agent-skills/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, taken verbatim from the developer-hub `policyAllowWriteContract` resource): > An `_Application` creates a new `Contract` node and links it via `:COVERS` to an existing `Vehicle` (owned by an existing `Company`) and via `:ACCEPTED` from an existing `Person`. ```cypher MATCH (subject:_Application)-[r1:HAS_AGREEMENT_WITH]->(company:Company)-[r2:OWNS]->(vehicle:Vehicle) MATCH (person:Person) ``` Variables: `subject`, `r1`, `company`, `r2`, `vehicle`, `person`. The new `Contract` node will be declared as a fresh `name` in `upsert_nodes`; the two new relationships will reference `vehicle`, `person`, and the fresh `name` as endpoints. ### 2. Author the policy with both `node_types` and `relationship_types` Build the policy JSON with five blocks: - `meta.policy_version` - currently `1.0-ciq`. - `subject.type` - `_Application` for the running example. - `condition.cypher` and `condition.filter` - the cypher matches the subject and existing endpoints; the filter pins them by `external_id` (`$_appId` plus `$vehicleID`, `$personID`). - `allowed_upserts.nodes.node_types` - the new node's label (e.g. `["Contract"]`). - `allowed_upserts.relationships.relationship_types` - one triple per new relationship, matching the directions and labels. A complete combined-create policy for the running example: see [`assets/policy-create-contract.json`](/agent-skills/indykite-ciq-create-node-with-link/assets/policy-create-contract.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-create-node-with-link/assets/policy-create-contract.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 schema deep-dive (how `node_types` and `relationship_types` interact, why direction matters, what `existing_nodes` would add) see [`references/policy-reference.md`](/agent-skills/indykite-ciq-create-node-with-link/references/policy-reference.md). ### 3. Create the Knowledge Query with both `upsert_nodes` and `upsert_relationships` The Knowledge Query has two write arrays: **`upsert_nodes`** - declares the new node. Same shape as in [`indykite-ciq-create-node`](/agent-skills/indykite-ciq-create-node/SKILL.md): - `name` - fresh variable name (not in cypher), e.g. `contract`. - `type` - node label, must match `allowed_upserts.nodes.node_types`. - `external_id` - required for new nodes; usually `$param`. - `labels` - optional array of extra labels attached alongside `type`. Chiefly used to create **identity nodes** - see the note below. - `properties` - array of `{type, value, metadata?}` items. > **Identity nodes.** The Knowledge Query has no `is_identity` field - that flag belongs to the Capture API. In the IKG, identity status is carried by the `DigitalTwin` label; Capture's `is_identity: true` is shorthand for adding it at ingest. The CIQ equivalent is `"labels": ["DigitalTwin"]` on the `upsert_nodes` entry. The label goes in `labels` only - the policy's `node_types` whitelist checks `type`, so `DigitalTwin` is never listed there. Create the node as an identity node whenever it must act as a `2.0-kbac` subject: a non-identity subject makes every `2.0-kbac` decision silently `false` (`3.0-kbac` does not require it). To confirm the label landed, run a `2.0-kbac` evaluation with the new node as subject. **`upsert_relationships`** - declares each new relationship. Same shape as in [`indykite-ciq-create-relationship`](/agent-skills/indykite-ciq-create-relationship/SKILL.md), with one important twist: - `name` - fresh variable name for each new relationship (e.g. `r3`, `r4`). - `source` - variable name. **Can be a cypher variable** (existing node) **or the `name` of an `upsert_nodes` entry** (the just-created node). - `target` - same: cypher variable or `upsert_nodes` `name`. - `type` - must match the policy's `relationship_types`. That `source`/`target` flexibility is what makes the combined operation work: `r3` connects the just-created `contract` to the existing `vehicle`; `r4` connects the existing `person` to the just-created `contract`. A complete combined-create Knowledge Query for the running example: see [`assets/knowledge-query-create-contract.json`](/agent-skills/indykite-ciq-create-node-with-link/assets/knowledge-query-create-contract.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-create-node-with-link/assets/knowledge-query-create-contract.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 (which arrays interact, response shape covering both new nodes and new relationships) live in [`references/knowledge-query-reference.md`](/agent-skills/indykite-ciq-create-node-with-link/references/knowledge-query-reference.md). ### 4. Authenticate and execute The execute endpoint is the same as for every other CIQ operation: ```text POST /contx-iq/v1/execute ``` For the `_Application` subject: - `X-IK-ClientKey: ` - required. - `Authorization: Bearer …` - omit. Request: ```json { "id": "", "input_params": { "vehicleID": "car2", "personID": "ryan", "contract_external_id": "ct853", "contractNumber": "rbjh853" } } ``` A runnable shell helper: [`scripts/execute.sh`](/agent-skills/indykite-ciq-create-node-with-link/scripts/execute.sh). Full execute reference: [`references/execution-reference.md`](/agent-skills/indykite-ciq-create-node-with-link/references/execution-reference.md). ### 5. Verify the response and confirm the wiring A successful combined-create returns the new node's projection plus the new relationships' identifiers: ```json { "data": [ { "nodes": { "contract.external_id": "ct853", "contract.property.number": "rbjh853" }, "relationships": { "r3": { "Id": …, "ElementId": "…", "StartId": …, "EndId": … }, "r4": { "Id": …, "ElementId": "…", "StartId": …, "EndId": … } } } ] } ``` If the response is **not** what you expected, walk this list: 1. **Both whitelist entries present.** The KQ's `upsert_nodes[].type` must be in `allowed_upserts.nodes.node_types`, and each `upsert_relationships[]` triple must be in `allowed_upserts.relationships.relationship_types`. Either mismatch → `403`. 2. **Endpoints exist.** Every cypher variable the relationships reference (`vehicle`, `person`) must resolve to a real node. If `MATCH` finds no rows, the operation has nothing to wire - `200` with empty `data`. 3. **Cross-references match.** The new node's `name` in `upsert_nodes` (e.g. `contract`) must be exactly the same string used in `upsert_relationships[].source` or `target`. Typos here silently produce wiring failures. 4. **Direction matches.** Relationship triples encode direction. `(Contract)-[:COVERS]->(Vehicle)` is different from `(Vehicle)-[:COVERS]->(Contract)`. 5. **All `$param`s present.** The `contract_external_id`, `contractNumber`, `vehicleID`, `personID` all need to be in `input_params`. For other failure modes see [`references/troubleshooting.md`](/agent-skills/indykite-ciq-create-node-with-link/references/troubleshooting.md). ## Outcome When this skill has been applied successfully: - A combined-create CIQ policy exists; it has a single `subject.type`, a Cypher pattern matching the subject and existing endpoint nodes, partial filters, and *both* `allowed_upserts.nodes.node_types` *and* `allowed_upserts.relationships.relationship_types` populated. - A Knowledge Query references the policy and lists the new node in `upsert_nodes` and one or more new relationships in `upsert_relationships` (with the new node's `name` referenced as a `source` or `target`). - One `POST /contx-iq/v1/execute` returns the new node's projection plus the new relationships' identifiers. - A follow-up read confirms the new entity is wired into the graph. ## Files in this skill - [`references/policy-reference.md`](/agent-skills/indykite-ciq-create-node-with-link/references/policy-reference.md) - combined `node_types` + `relationship_types`, optional `existing_nodes` for hybrid create-and-update flows. - [`references/knowledge-query-reference.md`](/agent-skills/indykite-ciq-create-node-with-link/references/knowledge-query-reference.md) - `upsert_nodes` + `upsert_relationships` interaction, `source`/`target` cross-referencing, identity nodes via `labels`, multi-relationship patterns. - [`references/execution-reference.md`](/agent-skills/indykite-ciq-create-node-with-link/references/execution-reference.md) - request/response, atomicity guarantees, idempotence on rerun. - [`references/troubleshooting.md`](/agent-skills/indykite-ciq-create-node-with-link/references/troubleshooting.md) - `403` / empty-data / wiring-mismatch / cross-reference patterns. - [`assets/policy-create-contract.json`](/agent-skills/indykite-ciq-create-node-with-link/assets/policy-create-contract.json) - the canonical insurance-Contract example, lifted from `policyAllowWriteContract` in the developer-hub resources. - [`assets/knowledge-query-create-contract.json`](/agent-skills/indykite-ciq-create-node-with-link/assets/knowledge-query-create-contract.json) - matching Knowledge Query. - [`scripts/execute.sh`](/agent-skills/indykite-ciq-create-node-with-link/scripts/execute.sh) - Bash helper. ## Agent-specific notes This skill uses generic markdown instructions and works across all agents listed in the [README](/agent-skills/README.md). The agent needs to be able to issue HTTP requests. 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 how `upsert_nodes` and `upsert_relationships` cross-reference. - [Developer-hub resources - `policyAllowWriteContract` and `knowledgeQueryAllowWriteContract`](https://developer.indykite.com/resources) - the canonical insurance-Contract example this skill is built around. - [Music dataset tutorial - Chapter 9 "Knowledge Queries"](https://developer.indykite.com/tutorials/tutorial-music-dataset) - `kqb` write variants for context-aware ingestion patterns. - [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) --- --- name: indykite-ciq-create-node description: Author an IndyKite ContX IQ (CIQ) policy plus its Knowledge Query that creates a brand-new node in the IndyKite Graph (IKG), then run it via `POST /contx-iq/v1/execute`. Use when ingesting a new entity through CIQ - no relationship creation, no updates to existing nodes, 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 - create a new node Create a brand-new node in the IndyKite Graph (IKG) through a ContX IQ policy + Knowledge Query, run via `POST /contx-iq/v1/execute`. The policy declares an `allowed_upserts.nodes.node_types` whitelist of node labels that may be created, and the Knowledge Query's `upsert_nodes` array names the node, sets its `external_id`, and lists the properties to write. (CIQ writes use the same policy + Knowledge Query shape as reads.) This skill covers exactly that - node creation only. Other write paths are deliberately out of scope: - **Updating an existing node's properties** uses `allowed_upserts.nodes.existing_nodes` and a Knowledge Query `upsert_nodes` entry that references a variable from the policy's `cypher` (no `external_id`). Different field, different KQ shape. - **Creating relationships** uses `allowed_upserts.relationships.relationship_types` (`{type, source_node_label, target_node_label}` triples) and the Knowledge Query's `upsert_relationships` array. - **Deletes** use `allowed_deletes` and `delete_nodes` / `delete_relationships`. For reads, see [`indykite-ciq-read`](/agent-skills/indykite-ciq-read/SKILL.md). ## When to use Activate this skill when the user: - wants to **create** a new node in the IKG through CIQ (e.g. ingest a new `Track`, `Document`, `Account`, or other entity); - is authoring an `_Application`-subject "catalog write" policy + Knowledge Query - the typical pattern for ETL / system-side ingestion; - is parameterising the new node's `external_id` and properties from execute-time `input_params`; - is debugging a `403` / `422` from a `POST /contx-iq/v1/execute` call that should have created a node but didn't. Do **not** activate this skill when the user: - wants to **read** data from the IKG - use [`indykite-ciq-read`](/agent-skills/indykite-ciq-read/SKILL.md); - wants to **update** an existing node's properties - different policy field (`existing_nodes`) and KQ shape; - wants to **create relationships** between nodes - different policy field (`relationship_types`) and KQ array (`upsert_relationships`); - wants to **delete** anything - different policy field (`allowed_deletes`) and KQ array; - is using the Capture API (`POST /capture/v1/nodes`) or Terraform to ingest data instead of CIQ - those are separate ingestion paths. ## 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 **node label** the new node will use (`Track`, `Document`, `Customer`, etc.) - already part of the project's data model. - For non-`_Application` subjects, the **subject's** node already in the IKG (CIQ doesn't create the subject; it authorizes against it). - A **plan for `external_id`** - the new node's stable identifier. Two choices the caller must make every time: hard-code it in the policy/KQ (rare), or supply it as a `$param` at execute time (common). If any of these are missing, stop and tell the user - fixing them first is much cheaper than debugging a vague `403` or `422`. ## Steps ### 1. Pick the subject and 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 `. | `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 catalog ingestion); a `Person` variant - for example, a user creating their own Playlist - differs only in `subject.type`, the filter, and the execute headers. **Cypher anchor** - even a write-only policy needs a `MATCH` clause that anchors to the subject. The new node is *not* matched in `cypher`; it's declared in the Knowledge Query's `upsert_nodes`. Working example (used throughout this skill): > System-side catalog ingestion: an `_Application` creates a new `Track` node, supplying `external_id`, `title`, and `loudness` at execute time. ```cypher MATCH (subject:_Application) ``` That's the entire `cypher` - just enough to identify the subject. The `Track` does not appear here. ### 2. Author the policy with `allowed_upserts.nodes.node_types` 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` - anchor to the subject. For `_Application`, filter on `subject.external_id = $_appId` (a reserved value auto-filled from the AppAgent at execute time). - `allowed_upserts.nodes.node_types` - array of node labels the Knowledge Query may **create** as new nodes. **Omit** `allowed_reads`, `allowed_deletes`, and the other `allowed_upserts` sub-fields if this policy only creates nodes. Leaving them out is the supported way to forbid those operations. A complete create-only policy for the running example: see [`assets/policy-create-track.json`](/agent-skills/indykite-ciq-create-node/assets/policy-create-track.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-create-node/assets/policy-create-track.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 (operators, attribute conventions, why we omit `existing_nodes` and `allowed_reads`) see [`references/policy-reference.md`](/agent-skills/indykite-ciq-create-node/references/policy-reference.md). ### 3. Create the Knowledge Query with `upsert_nodes` The Knowledge Query references the policy and lists what to write. Each entry in `upsert_nodes` describes one node to create: - `name` - a **distinct** variable name not used in the policy's `cypher`. This is the variable other arrays (`nodes`, `relationships`) reference. - `type` - the node label. Must be in the policy's `allowed_upserts.nodes.node_types`. - `external_id` - **required for new nodes**. Hardcode for one-off writes, or use `$param` (the common case) so the caller supplies it at execute time. - `labels` - optional array of extra labels attached alongside `type`. Chiefly used to create **identity nodes** - see the note below. - `properties` - array of `{type, value, metadata?}` items. The `type` (property name) must be hardcoded; the `value` may be hardcoded or `$param`. Echo the new node back in the response by listing its variable name in the top-level `nodes` array. > **Identity nodes.** The Knowledge Query has no `is_identity` field - that flag belongs to the Capture API. In the IKG, identity status is carried by the `DigitalTwin` label; Capture's `is_identity: true` is shorthand for adding it at ingest. The CIQ equivalent is `"labels": ["DigitalTwin"]` on the `upsert_nodes` entry. The label goes in `labels` only - the policy's `node_types` whitelist checks `type`, so `DigitalTwin` is never listed there. Create the node as an identity node whenever it must act as a `2.0-kbac` subject: a non-identity subject makes every `2.0-kbac` decision silently `false` (`3.0-kbac` does not require it). To confirm the label landed, run a `2.0-kbac` evaluation with the new node as subject. A complete Knowledge Query for the running example: see [`assets/knowledge-query-create-track.json`](/agent-skills/indykite-ciq-create-node/assets/knowledge-query-create-track.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-create-node/assets/knowledge-query-create-track.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) - what `execute` and the MCP `ciq_execute` tool will reference. Schema details for every Knowledge Query field, including the protected property names you cannot set: [`references/knowledge-query-reference.md`](/agent-skills/indykite-ciq-create-node/references/knowledge-query-reference.md). ### 4. Authenticate and execute The execute endpoint is the same as for reads: ```text POST /contx-iq/v1/execute ``` Authentication for the running `_Application`-subject example: - `X-IK-ClientKey: ` - required. - `Authorization: Bearer …` - **omit** for `_Application` subjects. The AppAgent itself authenticates the subject, and `$_appId` is auto-filled from the application's `external_id`. For Person-subject create flows, add `Authorization: Bearer ` and the policy's filter on `subject.external_id = $token.sub` will pin the cypher anchor to that user. Request body: ```json { "id": "", "input_params": { "track_external_id": "track-99", "track_title": "New Hot Track", "track_loudness": -7.5 } } ``` A runnable shell helper: [`scripts/execute.sh`](/agent-skills/indykite-ciq-create-node/scripts/execute.sh). Full execute reference (auth combinations, response shape, error codes): [`references/execution-reference.md`](/agent-skills/indykite-ciq-create-node/references/execution-reference.md). ### 5. Verify the response and confirm the new node A successful create execute returns the new node's projection: ```json { "data": [ { "nodes": { "newTrack.external_id": "track-99", "newTrack.property.title": "New Hot Track", "newTrack.property.loudness": -7.5 } } ] } ``` If the response is **not** what you expected, walk this list before changing the policy or KQ: 1. **The label is whitelisted.** The Knowledge Query's `upsert_nodes[].type` must be in the policy's `allowed_upserts.nodes.node_types`. Mismatch → `403`. 2. **`external_id` is set.** Required for new-node creation. If you're parameterising it (`"$track_external_id"`), the caller must supply it in `input_params`. Missing → `422 invalid_argument: missing or wrong input params`. 3. **`name` doesn't collide with a cypher variable.** The variable name in `upsert_nodes[].name` should be **fresh** - not a name that already appears in the policy's `cypher`. If it collides, the policy thinks you're updating an existing match instead of creating. 4. **Property names aren't in the protected set.** `_service`, `create_time`, `external_id`, `id`, `type`, `update_time` cannot be set as properties - they're managed by the platform. 5. **The node didn't already exist.** Re-running with the same `external_id` upserts (updates) instead of creating; the response will look similar but no new node is added. For other failure modes (auth shape wrong, malformed JSON, subject filter mismatch) see [`references/troubleshooting.md`](/agent-skills/indykite-ciq-create-node/references/troubleshooting.md). ## Outcome When this skill has been applied successfully: - A create-only CIQ policy exists in the project; it has a single `subject.type`, a Cypher pattern that anchors to the subject, optional partial filters, and an `allowed_upserts.nodes.node_types` whitelist - no `allowed_reads`, no `allowed_deletes`, no `existing_nodes`. - A Knowledge Query references that policy and lists exactly one new node in `upsert_nodes` with a distinct `name`, the right `type`, an `external_id`, and the properties to set. - `POST /contx-iq/v1/execute` (or the MCP `ciq_execute` tool) returns the new node's projection on success. - A follow-up read query (e.g. via [`indykite-ciq-read`](/agent-skills/indykite-ciq-read/SKILL.md)) finds the new node in the IKG. ## Files in this skill - [`references/policy-reference.md`](/agent-skills/indykite-ciq-create-node/references/policy-reference.md) - write-focused policy schema, `allowed_upserts.nodes` deep-dive (existing vs node_types), why other blocks are omitted. - [`references/knowledge-query-reference.md`](/agent-skills/indykite-ciq-create-node/references/knowledge-query-reference.md) - `upsert_nodes` schema, properties + metadata, identity nodes via `labels`, protected property names, returning the new node. - [`references/execution-reference.md`](/agent-skills/indykite-ciq-create-node/references/execution-reference.md) - `POST /contx-iq/v1/execute` for writes, auth combinations including `_Application` reserved `$_appId`, response shape. - [`references/troubleshooting.md`](/agent-skills/indykite-ciq-create-node/references/troubleshooting.md) - `403` / `422` / duplicate `external_id` / missing properties patterns. - [`assets/policy-create-track.json`](/agent-skills/indykite-ciq-create-node/assets/policy-create-track.json) - runnable create-only policy for the `_Application` → new `Track` example. - [`assets/knowledge-query-create-track.json`](/agent-skills/indykite-ciq-create-node/assets/knowledge-query-create-track.json) - matching Knowledge Query. - [`scripts/execute.sh`](/agent-skills/indykite-ciq-create-node/scripts/execute.sh) - Bash helper that posts to `/contx-iq/v1/execute` with the right headers. ## Agent-specific notes This skill uses generic markdown instructions and works across all agents listed in the [README](/agent-skills/README.md). The agent needs to be able to issue HTTP requests (`curl`, an HTTP client, or the IndyKite Terraform provider - see References). 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) - [Music dataset tutorial - Chapter 8 "ContX IQ policies" and Chapter 9 "Knowledge Queries"](https://developer.indykite.com/tutorials/tutorial-music-dataset) - concrete read/write/delete variants against a real graph. - [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 - `indykite_authorization_policy` and `indykite_knowledge_query`](https://registry.terraform.io/providers/indykite/indykite/latest/docs) - [Credentials guide](https://developer.indykite.com/guides/guide-credentials) --- --- name: indykite-ciq-create-relationship description: Author an IndyKite ContX IQ (CIQ) policy plus its Knowledge Query that creates a brand-new relationship between two existing nodes in the IndyKite Graph (IKG), then run it via `POST /contx-iq/v1/execute`. Use when wiring two existing entities together through CIQ - no new nodes, no relationship updates, 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 - create a new relationship Create a brand-new relationship between two nodes that already exist in the IndyKite Graph (IKG), driven by a ContX IQ policy + Knowledge Query and run via `POST /contx-iq/v1/execute`. The policy declares an `allowed_upserts.relationships.relationship_types` whitelist of `{type, source_node_label, target_node_label}` triples and matches the two endpoint nodes in its `cypher`; the Knowledge Query's `upsert_relationships` array names the new relationship and references those **`cypher` variables** as `source` and `target`. The endpoint nodes must already exist - only the relationship is created. This skill covers exactly that - relationship creation between two pre-existing nodes. Other paths are deliberately out of scope: - **Creating a new node** uses `allowed_upserts.nodes.node_types` and `upsert_nodes` - see [`indykite-ciq-create-node`](/agent-skills/indykite-ciq-create-node/SKILL.md). - **Creating a relationship to a brand-new node** combines both: an `upsert_nodes` entry for the new node plus an `upsert_relationships` entry that points its `source` or `target` at the fresh `name`. The schema supports this; this skill keeps the example tight to two existing endpoints for clarity. See "Adapting for a fresh endpoint" near the bottom. - **Updating an existing relationship's properties** uses `allowed_upserts.relationships.existing_relationships`. - **Deletes** use `allowed_deletes.relationships`. For reads, see [`indykite-ciq-read`](/agent-skills/indykite-ciq-read/SKILL.md). ## When to use Activate this skill when the user: - wants to **link two existing nodes** with a new relationship (e.g. `Person -[:ACCEPTED]-> Contract`, `Track -[:PLAYED_AT]-> Venue`, `User -[:OWNS]-> Document`); - is authoring an `_Application`-subject "catalog wiring" policy + Knowledge Query for system-side relationship ingestion; - is parameterising the source/target `external_id`s and (optionally) the new relationship's properties from execute-time `input_params`; - is debugging a `403` / `422` from a `POST /contx-iq/v1/execute` call that should have created a relationship but didn't. Do **not** activate this skill when the user: - wants to **create a new node** (with or without a relationship from another node) - use [`indykite-ciq-create-node`](/agent-skills/indykite-ciq-create-node/SKILL.md); - wants to **read** data from the IKG - use [`indykite-ciq-read`](/agent-skills/indykite-ciq-read/SKILL.md); - wants to **update an existing relationship's properties** - different policy field (`existing_relationships`) and KQ shape; - wants to **delete** a relationship - different policy field (`allowed_deletes.relationships`) and KQ array; - is using the Capture API (`POST /capture/v1/relationships`) or Terraform to ingest relationships instead of CIQ - those are separate ingestion paths. ## 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. - **Both endpoint nodes already in the IKG** with stable `external_id`s. CIQ doesn't seed them; this policy authorises wiring two existing nodes. - A **relationship label** (`PLAYED_AT`, `ACCEPTED`, `OWNS`, etc.) and the source/target node labels it connects. Both must match what the IKG schema already permits. - 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 `422`. ## Steps ### 1. Pick the subject and the cypher pattern **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 `. | `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 wiring); a `Person` variant - for example, a user accepting a Contract - differs only in `subject.type`, the filter, and the execute headers. **Cypher pattern** - must `MATCH` both endpoint nodes, **plus** the subject. Use disjoint `MATCH` clauses (separated by spaces) when the endpoints aren't connected through any other path you need. 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`](/agent-skills/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): > Music-dataset domain: an `_Application` adds a `PLAYED_AT` relationship from an existing `Track` to an existing `Venue`, given both `external_id`s. ```cypher MATCH (subject:_Application) MATCH (track:Track) MATCH (venue:Venue) ``` Variables the rest of the policy and KQ will reference: `subject`, `track`, `venue`. The new relationship does **not** appear in the cypher - it's declared in the KQ. If you need to constrain the endpoints to be reachable along an existing path before the new edge can be added, use a connected pattern instead - for example, you might require the Track and Venue to share an existing `:CATALOGED_BY` relationship before allowing the `PLAYED_AT` link. That decision belongs in the cypher. ### 2. Author the policy with `allowed_upserts.relationships.relationship_types` 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` - anchor the subject and pin the endpoints by `external_id`. For `_Application`, filter on `subject.external_id = $_appId` (reserved, auto-filled). For each endpoint, filter on its `external_id` against a `$param`. - `allowed_upserts.relationships.relationship_types` - array of `{type, source_node_label, target_node_label}` triples the Knowledge Query may **create** as new relationships. **Omit** `allowed_reads`, `allowed_deletes`, and the other `allowed_upserts` sub-fields if this policy only creates relationships. Omitting a block is the supported way to forbid that operation. A complete relationship-create policy for the running example: see [`assets/policy-create-played-at.json`](/agent-skills/indykite-ciq-create-relationship/assets/policy-create-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-create-relationship/assets/policy-create-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 (the `relationship_types` triple, why we omit `existing_relationships` and `node_types`) see [`references/policy-reference.md`](/agent-skills/indykite-ciq-create-relationship/references/policy-reference.md). ### 3. Create the Knowledge Query with `upsert_relationships` The Knowledge Query references the policy and lists what to write. Each entry in `upsert_relationships` describes one new relationship: - `name` - a **distinct** variable name not used in the policy's `cypher`. Convention: prefix with `new` or use a domain-specific noun. The response uses this name as the key for the new relationship's identifiers. - `source` - the variable name of the **source endpoint** from the policy's cypher. Must match what `relationship_types[].source_node_label` declares. - `target` - the variable name of the **target endpoint** from the policy's cypher. Must match `target_node_label`. - `type` - the relationship label. Must equal the `relationship_types[].type` in the policy. - `properties` - *optional*. Same `{type, value, metadata?}` shape as node properties - see [`references/knowledge-query-reference.md`](/agent-skills/indykite-ciq-create-relationship/references/knowledge-query-reference.md). Echo the new relationship back in the response by listing its variable name in the top-level `relationships` array. A complete Knowledge Query for the running example: see [`assets/knowledge-query-create-played-at.json`](/agent-skills/indykite-ciq-create-relationship/assets/knowledge-query-create-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-create-relationship/assets/knowledge-query-create-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). ### 4. Authenticate and execute The execute endpoint is the same as for reads and node-creates: ```text POST /contx-iq/v1/execute ``` Authentication for the running `_Application`-subject example: - `X-IK-ClientKey: ` - required. - `Authorization: Bearer …` - **omit** for `_Application`. The reserved `$_appId` is auto-filled from the application's `external_id`. For Person-subject linking flows, add `Authorization: Bearer ` and the policy's filter on `subject.external_id = $token.sub`. Request: ```json { "id": "", "input_params": { "track_external_id": "track-99", "venue_external_id": "venue-1" } } ``` A runnable shell helper: [`scripts/execute.sh`](/agent-skills/indykite-ciq-create-relationship/scripts/execute.sh). Full execute reference (auth, request/response, error semantics): [`references/execution-reference.md`](/agent-skills/indykite-ciq-create-relationship/references/execution-reference.md). ### 5. Verify the response and confirm the new relationship A successful create-relationship execute returns the projected nodes plus a `relationships` block keyed by the variable name with internal graph identifiers: ```json { "data": [ { "nodes": { "track.external_id": "track-99", "venue.external_id": "venue-1" }, "relationships": { "newPlayedAt": { "Id": 1152932499723124700, "ElementId": "5:3a2b09d5-…:1152932499723124736", "StartId": 0, "StartElementId": "4:3a2b09d5-…:0", "EndId": 15 } } } ] } ``` The `Id` / `ElementId` are the platform's internal identifiers for the new edge; you don't need to use them but they confirm the edge was written. If the response is **not** what you expected, walk this list before changing the policy or KQ: 1. **Both endpoints exist.** The policy's cypher needs to actually match - if `track.external_id` or `venue.external_id` isn't seeded, the cypher returns no rows and there's nothing for `upsert_relationships` to attach to. 2. **Triple matches.** The KQ's `(source, target, type)` must align with the policy's `relationship_types[]` triple - same labels (via the cypher variables) and same `type`. 3. **Variables exist in cypher.** `source` and `target` are **cypher variable names**, not labels. If you write `"source": "Track"` (the label) instead of `"source": "track"` (the variable), the request fails. 4. **`name` is fresh.** The new relationship's `name` must not collide with an existing variable in the policy's cypher. 5. **The relationship didn't already exist.** Re-running with the same source/target pair upserts (matches the existing edge) instead of creating a duplicate. For other failure modes (auth shape wrong, missing input_params, malformed JSON) see [`references/troubleshooting.md`](/agent-skills/indykite-ciq-create-relationship/references/troubleshooting.md). ## Adapting for a fresh endpoint If you need to create the **target node and the relationship in one execute** - say, create a new `Comment` and link it to an existing `Document` - combine this skill's pattern with [`indykite-ciq-create-node`](/agent-skills/indykite-ciq-create-node/SKILL.md): - Policy: include both `allowed_upserts.nodes.node_types` (for the new node label) and `allowed_upserts.relationships.relationship_types` (for the new edge). - Knowledge Query: an `upsert_nodes` entry for the new node, plus an `upsert_relationships` entry whose `source` or `target` is the fresh node's `name`. The two skills cover the parts; combining is just one extra `upsert_*` array entry on each side. Each operation must be whitelisted in the policy. ## Outcome When this skill has been applied successfully: - A relationship-create CIQ policy exists; it has a single `subject.type`, a Cypher pattern matching both endpoint nodes, partial filters pinning them by `external_id`, and an `allowed_upserts.relationships.relationship_types` whitelist - no `node_types`, no `existing_relationships`, no `allowed_reads` (unless intentionally added), no `allowed_deletes`. - A Knowledge Query references that policy and lists exactly one new relationship in `upsert_relationships` with a fresh `name`, source/target variables from cypher, and the right `type`. - `POST /contx-iq/v1/execute` returns `data` with the projected nodes and the new relationship's internal identifiers. - A follow-up read (e.g. via [`indykite-ciq-read`](/agent-skills/indykite-ciq-read/SKILL.md)) finds the new edge in the IKG. ## Files in this skill - [`references/policy-reference.md`](/agent-skills/indykite-ciq-create-relationship/references/policy-reference.md) - relationship-create policy schema, the `{type, source_node_label, target_node_label}` triple, why other blocks are omitted. - [`references/knowledge-query-reference.md`](/agent-skills/indykite-ciq-create-relationship/references/knowledge-query-reference.md) - `upsert_relationships` schema, optional properties, returning the new relationship. - [`references/execution-reference.md`](/agent-skills/indykite-ciq-create-relationship/references/execution-reference.md) - `POST /contx-iq/v1/execute` for relationship writes, auth combinations, response shape with `Id` / `ElementId` / `StartId` / `EndId`. - [`references/troubleshooting.md`](/agent-skills/indykite-ciq-create-relationship/references/troubleshooting.md) - symptom → cause → fix tables for `403` / `422` / no-match-on-cypher / variable-vs-label confusion. - [`assets/policy-create-played-at.json`](/agent-skills/indykite-ciq-create-relationship/assets/policy-create-played-at.json) - runnable `_Application` → `(Track)-[:PLAYED_AT]->(Venue)` policy. - [`assets/knowledge-query-create-played-at.json`](/agent-skills/indykite-ciq-create-relationship/assets/knowledge-query-create-played-at.json) - matching Knowledge Query. - [`scripts/execute.sh`](/agent-skills/indykite-ciq-create-relationship/scripts/execute.sh) - Bash helper that posts to `/contx-iq/v1/execute` with the right headers. ## Agent-specific notes This skill uses generic markdown instructions and works across all agents listed in the [README](/agent-skills/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) - [Music dataset tutorial - Chapter 8 "ContX IQ policies" and Chapter 9 "Knowledge Queries"](https://developer.indykite.com/tutorials/tutorial-music-dataset) - concrete read/write/delete variants against a real graph; the `kqb` pattern is the canonical relationship-create variant. - [Developer-hub resources - CIQ examples](https://developer.indykite.com/resources) - runnable `policyAllowUpsertRelationships` / `knowledgeQueryUpsertRelationships` pairs in the resource samples. - [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 - `indykite_authorization_policy` and `indykite_knowledge_query`](https://registry.terraform.io/providers/indykite/indykite/latest/docs) - [Credentials guide](https://developer.indykite.com/guides/guide-credentials) --- --- name: indykite-ciq-delete description: Author an IndyKite ContX IQ (CIQ) policy plus its Knowledge Query that deletes a node, a relationship, or one or more properties from the IndyKite Graph (IKG), then run it via `POST /contx-iq/v1/execute`. Use when removing data through CIQ - three modes (whole node, whole relationship, individual property) sharing the same policy/KQ shape. 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 - delete a node, relationship, or property Delete a node, a relationship, or individual properties from the IndyKite Graph (IKG), driven by a ContX IQ policy + Knowledge Query and run via `POST /contx-iq/v1/execute`. Two policy fields (`allowed_deletes.nodes`, `allowed_deletes.relationships`) and two Knowledge Query arrays (`delete_nodes`, `delete_relationships`) drive it: pass a variable name from the policy's `cypher` to delete the whole element, or `.` (or `.property.` for nodes) to delete only one property. Three modes share the same operation surface. | Mode | Policy field | KQ array | KQ entry shape | |-------------------------------|-------------------------------------------|-------------------------|----------------------------------------------------| | Delete a whole **node** | `allowed_deletes.nodes: ["car"]` | `delete_nodes` | `"car"` - the cypher variable | | Delete a single **node property** | `allowed_deletes.nodes: ["car.property.color"]` | `delete_nodes` | `"car.property.color"` - variable + property path | | Delete a whole **relationship** | `allowed_deletes.relationships: ["r"]` | `delete_relationships` | `"r"` - the cypher variable | | Delete a single **relationship property** | `allowed_deletes.relationships: ["r.status"]` | `delete_relationships` | `"r.status"` - variable + property name | This skill covers all four sub-cases. The runnable example focuses on the most common case (delete a property on the caller's own Person node), with the other modes documented in the references and the policy snippets. For creates, see [`indykite-ciq-create-node`](/agent-skills/indykite-ciq-create-node/SKILL.md) / [`indykite-ciq-create-relationship`](/agent-skills/indykite-ciq-create-relationship/SKILL.md). For property writes, see [`indykite-ciq-add-property`](/agent-skills/indykite-ciq-add-property/SKILL.md) / [`indykite-ciq-add-relationship-property`](/agent-skills/indykite-ciq-add-relationship-property/SKILL.md). For reads, see [`indykite-ciq-read`](/agent-skills/indykite-ciq-read/SKILL.md). ## When to use Activate this skill when the user: - wants to **remove** a node, relationship, or property from the IKG through CIQ; - is implementing a "right to be forgotten" or GDPR-style data-erasure path on an authenticated user's own data; - is unwiring a stale `:PLAYED_AT`, `:OWNS`, or other relationship that should no longer apply; - is clearing a property that was set by mistake (with the understanding that property-writes overwrite, but only `delete_nodes` actually removes a property); - is debugging a `403` / `200`-empty / `delete didn't happen` situation on a CIQ delete call. Do **not** activate this skill when the user: - wants to **create** a node or relationship - use [`indykite-ciq-create-node`](/agent-skills/indykite-ciq-create-node/SKILL.md) or [`indykite-ciq-create-relationship`](/agent-skills/indykite-ciq-create-relationship/SKILL.md); - wants to **set** a property - use [`indykite-ciq-add-property`](/agent-skills/indykite-ciq-add-property/SKILL.md) or [`indykite-ciq-add-relationship-property`](/agent-skills/indykite-ciq-add-relationship-property/SKILL.md); - wants to **read** data - use [`indykite-ciq-read`](/agent-skills/indykite-ciq-read/SKILL.md); - needs to delete a **protected property** (`_service`, `create_time`, `external_id`, `id`, `type`, `update_time`) - those are platform-managed and cannot be deleted. ## Prerequisites - An IndyKite **project**, **AppAgent**, and AppAgent **credentials**. - 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 node, relationship, or property already in the IKG**. - For non-`_Application` subjects, the **subject's** node also already in the IKG. If any of these are missing, stop and tell the user. ## 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 `. | `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 `Person` (user clears their own profile property); an `_Application` variant - for example, an ETL job pruning stale `:PLAYED_AT` edges - differs only in `subject.type`, the filter, and the execute headers. **Cypher pattern** - must `MATCH` the element you want to delete and bind it to a variable. For deleting a node or its property, match the node. For deleting a relationship or its property, match the relationship. 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`](/agent-skills/indykite-data-schema/SKILL.md)) - a typoed name silently matches nothing, and a delete whose pattern matches nothing is a no-op that still returns `200`. Working example (used throughout this skill): > A `Person` clears their own `music_mood` profile property. ```cypher MATCH (subject:Person) ``` Variable: `subject`. The KQ will reference this in `delete_nodes`. ### 2. Author the policy with `allowed_deletes` Build the policy JSON with four blocks: - `meta.policy_version` - currently `1.0-ciq`. - `subject.type` - `Person` for the running example. - `condition.cypher` and `condition.filter` - anchor the element. For `Person`, filter on `subject.external_id = $token.sub`. - `allowed_deletes` - at least one of `nodes` or `relationships`. Each entry is either a bare variable name (delete the whole element) or `.` / `.property.` (delete a property only). **Omit** `allowed_reads` and `allowed_upserts` if this policy only deletes. A complete delete-only policy for the running example: see [`assets/policy-delete-music-mood.json`](/agent-skills/indykite-ciq-delete/assets/policy-delete-music-mood.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-delete/assets/policy-delete-music-mood.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 four `allowed_deletes` modes, the wildcard form (`.*`), and why we omit `allowed_reads` and `allowed_upserts`, see [`references/policy-reference.md`](/agent-skills/indykite-ciq-delete/references/policy-reference.md). ### 3. Create the Knowledge Query with `delete_nodes` and/or `delete_relationships` The Knowledge Query references the policy. For each thing to delete, list it: - `delete_nodes` - array of variable names or `.property.` paths. Must match entries in the policy's `allowed_deletes.nodes`. - `delete_relationships` - array of variable names or `.` paths. Must match entries in the policy's `allowed_deletes.relationships`. A single KQ may delete multiple things in one execute (e.g. several properties at once, or a property *and* a relationship). Each entry is constrained by the policy's whitelist. A complete delete-property Knowledge Query for the running example: see [`assets/knowledge-query-delete-music-mood.json`](/agent-skills/indykite-ciq-delete/assets/knowledge-query-delete-music-mood.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-delete/assets/knowledge-query-delete-music-mood.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 for all four modes - including the protected property names you cannot delete and the wildcard syntax - live in [`references/knowledge-query-reference.md`](/agent-skills/indykite-ciq-delete/references/knowledge-query-reference.md). ### 4. Authenticate and execute The execute endpoint is the same as for every other CIQ operation: ```text POST /contx-iq/v1/execute ``` Authentication for the running `Person`-subject example: - `X-IK-ClientKey: ` - required. - `Authorization: Bearer ` - required. The token's `sub` claim drives `$token.sub`. For `_Application`-subject deletes, omit the Bearer header. Request: ```json { "id": "", "input_params": {} } ``` (For the running example, identity comes from the Bearer token, so `input_params` is empty. Other delete policies may require `$param`s for endpoint pinning - supply them as usual.) A runnable shell helper: [`scripts/execute.sh`](/agent-skills/indykite-ciq-delete/scripts/execute.sh). Full execute reference: [`references/execution-reference.md`](/agent-skills/indykite-ciq-delete/references/execution-reference.md). ### 5. Verify the response and confirm the delete A successful delete execute returns an empty or near-empty `data` array: ```json { "data": [ { "nodes": {} } ] } ``` The deletion happened if you reach `200`. To confirm: 1. **Re-run the same query.** A second call should return `200` again - deletes are idempotent (deleting an already-missing property is not an error). 2. **Run a paired read query** that projects the deleted property/element. The property is gone (`null`/missing) or the element is gone (empty `data`). 3. **Check the node's `update_time`** if you deleted a property from a node - the platform bumps it on every write or delete. If the response is **not** what you expected, walk this list: 1. **Variable in the right `allowed_deletes` field.** Property paths under nodes go in `allowed_deletes.nodes` (e.g. `"car.property.color"`); under relationships in `allowed_deletes.relationships` (e.g. `"r.status"`). 2. **Cypher matched the element.** If the cypher returns no rows, the delete has nothing to act on - `200` with empty `data`. That's not an error; the delete just didn't apply. 3. **Property name not in the protected set.** `_service`, `create_time`, `external_id`, `id`, `type`, `update_time` cannot be deleted. 4. **No `external_id` confusion.** Deleting a node deletes it entirely - you cannot "delete only the `external_id`" because that's a protected field. For other failure modes see [`references/troubleshooting.md`](/agent-skills/indykite-ciq-delete/references/troubleshooting.md). ## Outcome When this skill has been applied successfully: - A delete-only CIQ policy exists; it has a single `subject.type`, a Cypher pattern that resolves to the element(s) to delete, optional partial filters, and an `allowed_deletes` whitelist. - A Knowledge Query references that policy and lists `delete_nodes` and/or `delete_relationships` entries that match the policy's whitelist. - `POST /contx-iq/v1/execute` returns `200` and the targeted element(s) or property(ies) are gone. - A follow-up read confirms the deletion. ## Files in this skill - [`references/policy-reference.md`](/agent-skills/indykite-ciq-delete/references/policy-reference.md) - `allowed_deletes` deep-dive (four modes), the wildcard `.*` form, why other blocks are omitted. - [`references/knowledge-query-reference.md`](/agent-skills/indykite-ciq-delete/references/knowledge-query-reference.md) - `delete_nodes` and `delete_relationships` schemas, protected property names, multi-delete patterns. - [`references/execution-reference.md`](/agent-skills/indykite-ciq-delete/references/execution-reference.md) - `POST /contx-iq/v1/execute` for deletes, response shape, idempotence. - [`references/troubleshooting.md`](/agent-skills/indykite-ciq-delete/references/troubleshooting.md) - `403` / empty-`data` / "delete didn't happen" patterns. - [`assets/policy-delete-music-mood.json`](/agent-skills/indykite-ciq-delete/assets/policy-delete-music-mood.json) - runnable Person-subject "delete own property" policy. - [`assets/knowledge-query-delete-music-mood.json`](/agent-skills/indykite-ciq-delete/assets/knowledge-query-delete-music-mood.json) - matching Knowledge Query. - [`scripts/execute.sh`](/agent-skills/indykite-ciq-delete/scripts/execute.sh) - Bash helper. ## Agent-specific notes This skill uses generic markdown instructions and works across all agents listed in the [README](/agent-skills/README.md). The agent needs to be able to issue HTTP requests. 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 for `allowed_deletes` and `delete_nodes` / `delete_relationships`. - [Music dataset tutorial - Chapter 9 "Knowledge Queries"](https://developer.indykite.com/tutorials/tutorial-music-dataset) - `kqc` is the canonical delete variant in the read/write/delete naming convention. - [Developer-hub resources - CIQ examples](https://developer.indykite.com/resources) - `policyDeleteProperty` and the `Cars` collection's delete patterns. - [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) --- --- name: indykite-ciq-read description: Author a read-only IndyKite ContX IQ (CIQ) policy plus its Knowledge Query, then run it via `POST /contx-iq/v1/execute`. Use when exposing IKG nodes, relationships, or aggregate values as a parameterized read query - no upserts, 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 - read-only policy + Knowledge Query ContX IQ (CIQ) is IndyKite's context-aware data layer over the IKG — IndyKite's knowledge graph, a property-graph database queried with Cypher (the Neo4j / openCypher graph query language). A CIQ **policy** declares *what graph elements may be touched* and *under what conditions*; a CIQ **Knowledge Query** declares *what to do with them*; an **execution** call runs the Knowledge Query at runtime with concrete parameter values. This skill covers the **read-only** path: - A policy whose `condition.cypher` matches nodes and relationships and whose `allowed_reads` whitelists the variables the Knowledge Query may return. - A Knowledge Query that lists those variables in `nodes`, `relationships`, and/or `aggregate_values`. - An `execute` call that supplies values for the policy's partial filters (`$variable`) and returns the rows. `allowed_upserts` and `allowed_deletes` are intentionally **out of scope** here - leave them out entirely for read-only use. ## When to use Activate this skill when the user: - wants to **read** data from the IKG and shape the result through a CIQ policy + Knowledge Query; - is building the `(workflow, agent_list)` query the [`indykite-agent-gateway`](/agent-skills/indykite-agent-gateway/SKILL.md) skill consumes from ContX IQ; - is preparing a Knowledge Query that the [`indykite-mcp-server`](/agent-skills/indykite-mcp-server/SKILL.md) skill will run via `ciq_execute`; - is debugging a read CIQ that returns nothing or `403`s for a subject that should have access. Do **not** activate this skill when the user: - needs to **create, update, or delete** nodes or relationships through CIQ - that uses `allowed_upserts` / `allowed_deletes` and `upsert_*` / `delete_*` Knowledge Query fields, which this skill leaves out for clarity; - is calling **AuthZEN** for a yes/no authorization decision (use `authzen_evaluate` and the MCP skill); - is calling the **External Data Resolver** to fetch data from an external API at query time (different feature). ## Prerequisites - An IndyKite **project**, an **AppAgent**, and AppAgent **credentials** (the token that goes into `X-IK-ClientKey` at execution 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 **IKG already populated** with the nodes and relationships the policy will match. CIQ only filters and projects; it does not seed data. - A **subject type** to authenticate against - `Person`, `User`, `_Application`, etc. CIQ policies are restricted to a single subject type, so if you need two subjects, plan for two policies. If any of these are missing, stop and tell the user - fixing them first is much cheaper than debugging an opaque CIQ rejection. ## Steps ### 1. Pick the subject and the Cypher pattern **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 `. | `subject.external_id = $token.sub`. | A policy is restricted to a single subject type - if both should be allowed, write two policies. The subject's variable in `cypher` is conventionally named `subject`. The runnable example below uses `Person`; an `_Application` variant - for example, a service reading the catalog - differs only in `subject.type`, the filter, and the execute headers. **Cypher pattern** - the `MATCH` / `OPTIONAL MATCH` clauses naming every node and relationship the query will touch. Each one must have a **variable name** so the policy and Knowledge Query can reference it. 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`](/agent-skills/indykite-data-schema/SKILL.md)) - a typoed name silently matches nothing. Working example used throughout this skill: > A `Person` (subject) `OWNS` `Car`s. Given a person's `external_id`, return the cars they own. ```cypher MATCH (subject:Person)-[r:OWNS]->(car:Car) ``` Variables: `subject`, `r`, `car`. ### 2. Author the read-only CIQ policy Build the policy JSON. For a read-only policy you need three things and only three things: - `meta.policy_version` - currently `1.0-ciq`. - `subject.type` - the chosen subject type. - `condition.cypher` and (optionally) `condition.filter` - the pattern and any filters. Use `$varname` to mark **partial filters** that will be supplied at execution time. - `allowed_reads` - list every variable the Knowledge Query will be allowed to return. Use `.*` to allow all properties of a node/relationship, or `.property.` for a single property. Skip `allowed_upserts` and `allowed_deletes` entirely - omitting them is the supported way to forbid writes. A complete read-only policy for the running example: see [`assets/policy-read-cars.json`](/agent-skills/indykite-ciq-read/assets/policy-read-cars.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' assets/policy-read-cars.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 (every operator, every attribute pattern, what `token_filter` is for) see [`references/policy-reference.md`](/agent-skills/indykite-ciq-read/references/policy-reference.md). ### 3. Create the Knowledge Query A read-only Knowledge Query references the policy and lists what to return. The four read-relevant fields are: - `nodes` - node variables (or `.property.`) to include in the response. - `relationships` - relationship variables to include. - `aggregate_values` - variables produced by aggregate functions in `cypher` (e.g. `COLLECT(...) AS xs` → `"xs"`). - `batch_read` - set to `true` only when you expect a result set big enough to risk the default timeout; raises the timeout to 5 minutes. A complete Knowledge Query for the running example: see [`assets/knowledge-query-read-cars.json`](/agent-skills/indykite-ciq-read/assets/knowledge-query-read-cars.json). Create it through the Config API (with `POLICY_ID` set to the policy's GID from the previous step): ```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' assets/knowledge-query-read-cars.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). This is what `execute` (and the MCP `ciq_execute` tool) will reference. Schema details for every Knowledge Query field, including `upsert_*` and `delete_*` (omitted for the read case): [`references/knowledge-query-reference.md`](/agent-skills/indykite-ciq-read/references/knowledge-query-reference.md). ### 4. Execute the query The execute endpoint runs the Knowledge Query at runtime with concrete parameter values: ```text POST /contx-iq/v1/execute ``` Authentication: - Always: `X-IK-ClientKey: `. - If `subject.type` is **not** `_Application`: also `Authorization: Bearer `. The token's `sub` is the subject identifier. - If `subject.type` **is** `_Application`: the reserved `$_appId` parameter is auto-filled from the application's `external_id`; do not pass it in `input_params`. Request: ```json { "id": "", "input_params": { "person_external_id": "alice" } } ``` A runnable shell helper: [`scripts/execute.sh`](/agent-skills/indykite-ciq-read/scripts/execute.sh). The full execute reference (auth combinations, response shape, error semantics) lives in [`references/execution-reference.md`](/agent-skills/indykite-ciq-read/references/execution-reference.md). ### 5. Read the response and verify The response shape is: ```json { "data": [ { "nodes": { ".": "", ... } }, { "relationships": { ... } } ] } ``` One row per match, with `nodes` keyed `.` and `relationships` keyed similarly. If the policy or Knowledge Query whitelists a variable but the IKG has nothing matching, the data array is simply empty. If the response is **not** what you expected, walk through this check list before changing the policy: 1. **Variable in the response?** It must appear in both the policy's `allowed_reads.nodes` / `relationships` / `aggregate_values` *and* the Knowledge Query's `nodes` / `relationships` / `aggregate_values`. The intersection is what gets returned. 2. **Filter actually firing?** A typo in `attribute` (e.g. `subject.external_id` vs. `person.external_id`) silently matches nothing. Re-read the [attribute naming conventions](/agent-skills/indykite-ciq-read/references/policy-reference.md#attribute-naming-conventions). 3. **Subject set up correctly?** For non-`_Application` subjects, the Bearer token's `sub` is the subject identifier; without a token the subject is not bound and many policies match nothing. 4. **Data actually in the IKG?** Run a probe query against the same shape but with `IS NOT NULL` filters to confirm the data exists. ## Outcome When this skill has been applied successfully: - A read-only CIQ policy exists in the project; it has a single `subject.type`, a `cypher` pattern with named variables, optional partial filters, and an `allowed_reads` whitelist - but no `allowed_upserts` or `allowed_deletes`. - A Knowledge Query references that policy and lists exactly the variables it should return in `nodes` / `relationships` / `aggregate_values`. - `POST /contx-iq/v1/execute` (or the MCP `ciq_execute` tool) returns the expected rows for valid `input_params` and an empty `data` array for valid-but-non-matching ones. - The same Knowledge Query can be invoked from the [`indykite-mcp-server`](/agent-skills/indykite-mcp-server/SKILL.md) skill via `ciq_execute` without further changes. ## Files in this skill - [`references/policy-reference.md`](/agent-skills/indykite-ciq-read/references/policy-reference.md) - read-only policy schema, operators, attribute naming, partial filters, `token_filter` and step-up advice. - [`references/knowledge-query-reference.md`](/agent-skills/indykite-ciq-read/references/knowledge-query-reference.md) - Knowledge Query schema, including the read-only fields used here and a one-line description of every other field for context. - [`references/execution-reference.md`](/agent-skills/indykite-ciq-read/references/execution-reference.md) - `POST /contx-iq/v1/execute` request and response shape, auth combinations, common error codes. - [`assets/policy-read-cars.json`](/agent-skills/indykite-ciq-read/assets/policy-read-cars.json) - runnable read-only policy for the `Person -[:OWNS]-> Car` example. - [`assets/knowledge-query-read-cars.json`](/agent-skills/indykite-ciq-read/assets/knowledge-query-read-cars.json) - matching Knowledge Query. - [`scripts/execute.sh`](/agent-skills/indykite-ciq-read/scripts/execute.sh) - Bash helper that posts to `/contx-iq/v1/execute` with the right headers. ## Agent-specific notes This skill uses generic markdown instructions and works across all agents listed in the [README](/agent-skills/README.md). The agent needs to be able to issue HTTP requests (`curl`, an HTTP client, or the IndyKite Terraform provider - see References). 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) - [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 - `indykite_authorization_policy` and `indykite_knowledge_query`](https://registry.terraform.io/providers/indykite/indykite/latest/docs) - [Credentials guide](https://developer.indykite.com/guides/guide-credentials) - [External Data Resolver guide (out of scope here, but useful for follow-on work)](https://developer.indykite.com/guides/guide-external-data-resolver) --- --- name: indykite-data-schema description: Read the observed data schema of the IndyKite Knowledge Graph (IKG) via the Data Schema REST API (`GET /data-schema/v1/`) - a JGFv2 document listing every node type with its properties, value-type tallies, and labels, plus every (source, relation, target) relationship combination, with occurrence counts but never the data itself. Use before authoring Cypher for CIQ Knowledge Queries or KBAC policies (exact type and property spellings), to verify a Capture ingest landed, to detect schema drift, or to give an agent the graph's vocabulary - "what does our IKG look like?". Not for reading graph data (indykite-ciq-read), writing it (indykite-capture-* / indykite-ciq-*), or authoring policies (indykite-authzen-kbac-policies). 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 Data Schema - read the IKG's observed schema The IndyKite Knowledge Graph (IKG) has no up-front schema definition step: the schema *emerges* from the data ingested through the Capture API or ContX IQ upserts. The platform tracks that emergent schema - which node types exist, which properties they carry with which value types, and how the types are connected - and exposes it through the **Data Schema API** as a JSON Graph Format (JGF) v2 document. It is a schema-level view only: type names, property names, observed value types, and occurrence counts. It never returns the data itself, so it is safe to hand to tools and agents that should know the graph's *vocabulary* without seeing its contents. ## When to use Activate this skill when the user wants to: - discover the **exact spelling** of node types, relationship types, and property names before authoring Cypher - a CIQ policy or Knowledge Query ([`indykite-ciq-read`](/agent-skills/indykite-ciq-read/SKILL.md) and siblings) or a KBAC policy ([`indykite-authzen-kbac-policies`](/agent-skills/indykite-authzen-kbac-policies/SKILL.md)) references them literally, and a policy written against `givenName` matches nothing when the data was ingested as `given_name`; - **verify an ingest** - after a Capture batch, one GET confirms the expected types and properties landed and the counts sanity-check the volume (a `"Perosn"` typo shows up immediately as a surprise node type); - **detect schema drift or data-quality issues** - a property reporting `string: 4980, integer: 20` means an upstream source started sending the wrong type; - get a **meta-model overview** of the project's graph ("what does our IKG look like?") for onboarding, visualization, or impact analysis before a cleanup. Do **not** activate this skill when the user: - wants the **data itself** - that is a ContX IQ read ([`indykite-ciq-read`](/agent-skills/indykite-ciq-read/SKILL.md)); - wants to **write** nodes, relationships, or properties (the [`indykite-capture-*`](/agent-skills/README.md) and [`indykite-ciq-*`](/agent-skills/README.md) skills); - expects to **define or enforce** a schema - the response is descriptive, observed from ingested data; it is not a constraint definition you author. ## Prerequisites - An IndyKite **project** with an **AppAgent** and AppAgent **credentials** (the token that goes into `X-IK-ClientKey`) - see the [Credentials guide](https://developer.indykite.com/guides/guide-credentials). - **Data already ingested.** A project with an empty IKG returns `404 Not Found` - that is the normal "nothing ingested yet" answer, not a failure of this skill. ## Steps ### 1. Call the endpoint ```text GET /data-schema/v1/ ``` where `API_URL` is `https://eu.api.indykite.com` or `https://us.api.indykite.com`, matching the project's region. Authentication is the AppAgent credential in the `X-IK-ClientKey` header, as is, without any prefix. There are no parameters - the project is derived from the credential. A runnable shell helper builds the authenticated request: [`scripts/read-schema.sh`](/agent-skills/indykite-data-schema/scripts/read-schema.sh) — run with `--print` to preview the `curl` (host-pinned; token redacted). ### 2. Read the response The response is one `graph` object: - `graph.directed` - always `true`. - `graph.metadata` - `created_at` and `updated_at` timestamps of the schema. - `graph.nodes` - a **map keyed by node type**. Each entry's `metadata` holds `node_count`, a `properties` map (keyed by property name), `system_labels`, and `user_defined_labels` (each a list of `{ name, count }`). - `graph.edges` - one entry per **(source type, relation, target type)** combination: the `source` and `target` node types, the `relation` (relationship type), `directed`, and `metadata` with the edge `count` and its `properties` map. Each `properties` entry describes one property: how many times it occurs (`count`) and the observed value types with per-type tallies (`types`); node properties additionally carry a `metadata` map with the same statistics for their provenance fields. Trimmed example - one node type and one edge from a vehicle-rental graph: ```json { "graph": { "nodes": { "Car": { "metadata": { "node_count": 1, "properties": { "manufacturer": { "count": 1, "types": [ { "type": "string", "count": 1 } ] }, "seats": { "count": 1, "types": [ { "type": "integer", "count": 1 } ] } } } } }, "edges": [ { "source": "Person", "target": "Car", "relation": "CAN_DRIVE", "directed": true, "metadata": { "count": 1, "properties": { "valid_until": { "count": 1, "types": [ { "type": "string", "count": 1 } ] } } } } ] } } ``` The full field reference, a complete example response, and `jq` recipes for common questions ("which node types exist?", "which properties does `Person` have?") are in [`references/data-schema-reference.md`](/agent-skills/indykite-data-schema/references/data-schema-reference.md). ### 3. Use what it tells you - **Copy spellings, don't retype them.** Take node types, relationship types, and property names verbatim from the response into Cypher patterns, KBAC conditions, Capture payloads, and AuthZEN `subject.type` / `resource.type` fields. - **Check the edge direction.** `graph.edges` records the stored `source` → `target` direction; a Cypher pattern drawn in the opposite direction matches nothing. - **Treat mixed type tallies as a red flag.** More than one entry in a property's `types` list usually means an upstream source changed what it sends. - **Diff over time.** `graph.metadata.updated_at` tells you when the schema last changed; polling and diffing the response is a cheap monitor for upstream changes. - **Count before you delete.** The per-property and per-edge counts show how much data a cleanup would touch before you call the Capture delete endpoints. ## Outcome When this skill has been applied successfully: - `GET /data-schema/v1/` returns the project's observed schema as a JGFv2 `graph` document - node types with property and label statistics, and one `edges` entry per (source, relation, target) combination - or a `404` that correctly identifies an empty project. - Downstream Cypher (CIQ policies and Knowledge Queries, KBAC conditions) and Capture payloads use type and property spellings taken from the response instead of guesses. ## Files in this skill - [`references/data-schema-reference.md`](/agent-skills/indykite-data-schema/references/data-schema-reference.md) - endpoint, auth, full JGFv2 response field reference, complete example response, error codes, and `jq` recipes. - [`scripts/read-schema.sh`](/agent-skills/indykite-data-schema/scripts/read-schema.sh) - Bash helper that GETs `/data-schema/v1/` with the right header (host-pinned; `--print` to preview). ## Agent-specific notes This skill uses generic markdown instructions and works across all agents listed in the [README](/agent-skills/README.md). The agent needs to be able to issue HTTP requests (`curl` or an HTTP client). No Claude Code hooks, Cursor `@`-mentions, or Copilot workspace context are required. ## References - [IKG Data Schema guide (developer hub)](https://developer.indykite.com/guides/guide-data-schema) - [Data Schema API documentation](https://openapi.indykite.com/api-documentation/dataschema) - [Credentials guide](https://developer.indykite.com/guides/guide-credentials) - [JSON Graph Format (JGF)](https://jsongraphformat.info/) - the response format. --- --- name: indykite-mcp-server description: Make live IndyKite authorization decisions (AuthZEN/KBAC) and run ContX IQ graph queries from an AI agent over the Model Context Protocol - self-contained Bearer-token JSON-RPC calls on the stateless protocol (revision 2026-07-28), no session handshake and no bespoke REST wiring. Use when calling the IndyKite MCP server (server/discover, tools/list, authzen_evaluate, authzen_evaluations, authzen_search_*, ciq_execute), configuring an MCP server, or debugging its single Bearer-token auth. license: Apache-2.0 compatibility: Requires curl and bash 4+. Network access to eu.mcp.indykite.com or us.mcp.indykite.com, plus the OAuth IdP that issues Bearer tokens, is required at runtime. --- # IndyKite MCP Server The **IndyKite MCP server** lets an AI agent make authorization decisions - "can this subject do X on Y?" (AuthZEN/KBAC) - and read or write the IndyKite Graph (ContX IQ), directly through the [Model Context Protocol](https://modelcontextprotocol.io/) instead of bespoke REST calls. It speaks **JSON-RPC over HTTP POST**. This skill uses the **stateless protocol** (revision `2026-07-28` and later): no `initialize`/`initialized` handshake and no `Mcp-Session-Id` - every request is self-contained, carrying the protocol metadata in `params._meta` plus the standard MCP headers. Older revisions (e.g. `2025-11-25`) use a session handshake instead; that legacy style is summarized in [`references/architecture.md`](/agent-skills/indykite-mcp-server/references/architecture.md). Two regional endpoints exist: - **EU**: `https://eu.mcp.indykite.com` - **US**: `https://us.mcp.indykite.com` The full URL for one project is `/mcp/v1/`. ## When to use Activate this skill when the user: - needs to **call** the IndyKite MCP server (discover its capabilities, list tools/resources, or call AuthZEN/CIQ tools); - is **configuring** an MCP server for a project (`POST /configs/v1/mcp-servers`) and needs the field set; - is debugging a **`401`** that returned `.well-known/oauth-protected-resource` metadata - almost always a missing, expired, or wrongly-bound `Authorization: Bearer` token; - is wiring an LLM client (Claude Code, Cursor, Goose, the [MCP Go SDK](https://github.com/modelcontextprotocol/go-sdk), etc.) into the IndyKite MCP and needs the request shape; - is choosing between `authzen_evaluate`, `authzen_evaluations`, `authzen_search_resource`, `authzen_search_action`, and `ciq_execute`. Do **not** activate this skill when the user: - is asking about the IndyKite Agent Gateway (use the [`indykite-agent-gateway`](/agent-skills/indykite-agent-gateway/SKILL.md) skill - IAG protects A2A agents or MCP servers behind a gateway, a different product); - is calling AuthZEN or ContX IQ over their **direct REST APIs** (no MCP involved) - different endpoints, different auth shape; - is asking about the MCP **specification itself** rather than the IndyKite implementation. ## Prerequisites The MCP server will reject requests for a project until all of the following exist: - An IndyKite **project** with an **Application** and an **AppAgent** (with Authorization API + ContX IQ API permissions). The server uses this AppAgent to call IndyKite APIs at runtime, resolved server-side from the MCP server configuration's `app_agent_id` - the client no longer sends an AppAgent token. - A **Token Introspect** configuration on the project - used to validate inbound user Bearer tokens. - An **MCP server configuration** (`POST /configs/v1/mcp-servers`) that binds the runtime endpoint to the AppAgent (`app_agent_id`) and Token Introspect, and declares `scopes_supported`. Without this configuration, requests for the project are rejected. See [`references/configuration.md`](/agent-skills/indykite-mcp-server/references/configuration.md). - The project's **GID** (used in the URL path). - Captured **data and policies**: KBAC and/or CIQ policies and Knowledge Queries, depending on which tools the agent will call. If any of these are missing, stop and tell the user - fixing them first is much cheaper than debugging an opaque MCP rejection. ## The stateless request shape Every request in this skill carries the same scaffolding; only the `method` and its payload change. **In the body**, a `params._meta` object: ```json "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {}, "io.modelcontextprotocol/clientInfo": {"name": "curl", "version": "1.0"} } ``` The `protocolVersion` key is **required** - a request without it is treated as legacy session-based and will fail with `404 session not found`. `clientCapabilities` is required (`{}` if none); `clientInfo` is optional. **In the headers**: | Header | Value | |------------------------------------|------------------------------------------------------------------------------------------------| | `Authorization: Bearer ` | The user's OAuth access token - the only auth header. | | `Content-Type` | `application/json` | | `Accept` | `application/json, text/event-stream` - responses may arrive as an SSE stream. | | `Mcp-Protocol-Version` | `2026-07-28` | | `Mcp-Method` | Must equal the JSON-RPC `method` in the body; mismatch or absence is rejected. | | `Mcp-Name` | Required for `tools/call` (tool name), `resources/read` (resource URI), `prompts/get` (prompt name); must match the body. | No session is created and no `Mcp-Session-Id` header comes back. If a mixed-version client sends a stale `Mcp-Session-Id` alongside a `2026-07-28` `_meta`, the `_meta` wins and the header is ignored. The helper [`scripts/mcp-call.sh`](/agent-skills/indykite-mcp-server/scripts/mcp-call.sh) assembles all of this for any method. ## Steps ### 1. Resolve the URL and credentials Build the full MCP URL: `/mcp/v1/` where `` is `https://eu.mcp.indykite.com` or `https://us.mcp.indykite.com`. Get the values into shell variables: ```bash export BEARER_TOKEN="" # → Authorization: Bearer export MCP_URL="https://us.mcp.indykite.com" export PROJECT_GID="" ``` A single `Authorization: Bearer` header is the only auth header on every call. The AppAgent the server uses to call IndyKite APIs at runtime is resolved **server-side** from the MCP server configuration's `app_agent_id` - clients no longer send an `X-IK-ClientKey` AppAgent token. See [`references/architecture.md`](/agent-skills/indykite-mcp-server/references/architecture.md) for the rationale (the Bearer token identifies the user as the AuthZEN subject). ### 2. Probe the server with `server/discover` (optional but recommended) The stateless protocol adds a `server/discover` method that returns the server's capabilities and the protocol revisions it accepts - use it to confirm the endpoint speaks `2026-07-28` before anything else: ```bash curl -s -i -X POST "$MCP_URL/mcp/v1/$PROJECT_GID" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "Authorization: Bearer $BEARER_TOKEN" \ -H "Mcp-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: server/discover" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "server/discover", "params": { "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {}, "io.modelcontextprotocol/clientInfo": {"name": "curl", "version": "1.0"} } } }' ``` The response is `200` with `result.supportedVersions` (e.g. `["2026-07-28", "2024-11-05", …]`), `result.capabilities`, and the server's instructions - and no `Mcp-Session-Id` header. Or run `scripts/mcp-call.sh server/discover`. ### 3. Discover tools and resources Before calling tools, ask the server what's available. Three useful methods (each a self-contained POST with the same `_meta` and headers, changing `Mcp-Method`): - `tools/list` - what tools the agent may call (the canonical set is in [`references/tools.md`](/agent-skills/indykite-mcp-server/references/tools.md), but list it to verify what *this* deployment exposes). - `resources/list` - what resources the MCP server exposes. - `resources/read` with `uri: "indykite://knowledge-queries/"` (also sent as the `Mcp-Name` header) - agent-friendly descriptions of every CIQ Knowledge Query, including the parameters each one expects. The third one is especially important before any `ciq_execute` call: it tells the agent *which* `id` to pass and *what* `input_params` shape the query expects. ```bash scripts/mcp-call.sh tools/list scripts/mcp-call.sh resources/read 'indykite://knowledge-queries/' ``` ### 4. Call AuthZEN tools For authorization decisions, pick the right tool for the question: | Question | Tool | |-------------------------------------------------------------------------|----------------------------| | "Can subject X do action Y on resource Z?" | `authzen_evaluate` | | "Run several of those checks at once" | `authzen_evaluations` | | "Which resources of type T can subject X do Y on?" | `authzen_search_resource` | | "Which actions can subject X do on resource Z?" | `authzen_search_action` | Each tool is invoked through the MCP `tools/call` method with `name` and `arguments` in `params` next to `_meta`, plus the `Mcp-Method: tools/call` and `Mcp-Name: ` headers. Schemas and one-call examples are in [`references/tools.md`](/agent-skills/indykite-mcp-server/references/tools.md). One non-obvious convention: when the subject is the authenticated caller, `subject_id` is the **`sub` claim of the Bearer token**, not a separately-supplied user identifier. ```bash scripts/mcp-call.sh tools/call authzen_evaluate \ '{"subject_type":"Person","subject_id":"alice","resource_type":"Car","resource_id":"cadillacv16","action_name":"CAN_DRIVE"}' ``` ### 5. Call CIQ tools `ciq_execute` runs a Knowledge Query against the IndyKite Graph (read or write). Two arguments: - `id` - GID **or** name of the Knowledge Query to run. - `input_params` - the partial parameters from the Knowledge Query **and its policy**; the exact set is documented in the query's description. Always discover queries first via `resources/read` on `indykite://knowledge-queries/` so the agent passes the right parameters. ### 6. Read, interpret, and audit responses Responses may arrive either as plain JSON or as an **SSE stream** (that is what the `Accept: application/json, text/event-stream` header allows) - in the SSE case the JSON-RPC message is in the `data:` line of the event. The JSON-RPC response echoes the request's `id`, with `result` on success or `error` on failure. For AuthZEN, the meaningful payload is a `text` content item in `result.content` whose body holds the JSON decision; for CIQ, it is the rows the query returned. Service-side errors (configuration broken, scopes missing) usually surface as JSON-RPC `error` objects; a protocol revision the server does not support returns `400` with error code `-32022` naming the `requested` and `supported` versions; transport problems (auth, connectivity) come back as HTTP `4xx`/`5xx` *before* JSON-RPC even runs - see [`references/troubleshooting.md`](/agent-skills/indykite-mcp-server/references/troubleshooting.md). ## Legacy session-based clients Protocol revisions **before** `2026-07-28` (e.g. `2025-11-25`) use a session handshake: `initialize` → capture the `Mcp-Session-Id` response header → `notifications/initialized` → send the header on every call. Both styles authenticate the same way and expose the same tools and resources; only use the legacy style when the client library cannot send the `_meta`-based requests. The lifecycle and rules are in [`references/architecture.md`](/agent-skills/indykite-mcp-server/references/architecture.md). ## Outcome When this skill has been applied successfully: - An MCP server configuration exists for the project (with `app_agent_id` set) and `enabled` is `true`. - The agent has `BEARER_TOKEN` (user OAuth access token) in scope and sends it as the sole `Authorization: Bearer` auth header. - `server/discover` returns `2026-07-28` among `result.supportedVersions`, and no `Mcp-Session-Id` header appears on any response. - `tools/list` and `resources/read` on `indykite://knowledge-queries/` enumerate what the agent can call. - AuthZEN decisions and CIQ query results come back over JSON-RPC and the agent uses them in its workflow. ## Files in this skill - [`references/architecture.md`](/agent-skills/indykite-mcp-server/references/architecture.md) - protocol styles (stateless `2026-07-28` vs legacy sessions), single Bearer-token auth with server-side AppAgent resolution, RFC 9728 `401` behavior. - [`references/configuration.md`](/agent-skills/indykite-mcp-server/references/configuration.md) - `POST /configs/v1/mcp-servers` field reference and example payload. - [`references/tools.md`](/agent-skills/indykite-mcp-server/references/tools.md) - schemas and examples for every AuthZEN and CIQ tool. - [`references/troubleshooting.md`](/agent-skills/indykite-mcp-server/references/troubleshooting.md) - symptom-to-cause map. - [`scripts/mcp-call.sh`](/agent-skills/indykite-mcp-server/scripts/mcp-call.sh) - Bash helper that makes one stateless MCP call (builds the `_meta` object and MCP headers for any method). Requires `MCP_URL`, `PROJECT_GID`, `BEARER_TOKEN` in the environment, and `curl` on `PATH`. ## Agent-specific notes This skill uses generic markdown instructions and works across all agents listed in the [README](/agent-skills/README.md). It assumes the agent can issue HTTP requests (Bash + `curl`, an HTTP MCP client, or an SDK such as the [MCP Go SDK](https://github.com/modelcontextprotocol/go-sdk)). No Claude Code hooks, Cursor `@`-mentions, or Copilot workspace context are required. ## References - [How to use the MCP server (IndyKite developer hub)](https://developer.indykite.com/guides/guide-mcp) - [Model Context Protocol specification](https://modelcontextprotocol.io/) - [MCP Go SDK](https://github.com/modelcontextprotocol/go-sdk) - [`POST /mcp-servers` API reference](https://openapi.indykite.com/api-documentation-config/#tag/mcp-servers/POST/mcp-servers) - [`POST /token-introspects` API reference](https://openapi.indykite.com/api-documentation-config#POST/token-introspects) - [`POST /application-agent-credentials` API reference](https://openapi.indykite.com/api-documentation-config#POST/application-agent-credentials) - [RFC 9728 - Protected Resource Metadata](https://datatracker.ietf.org/doc/html/rfc9728)