Back to all tutorials
ContX IQ

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.

Download the Postman collection to follow this tutorial:

music-dataset.postman_collection.json (Postman Collection format v2.1.0, ~18 MB - includes the inline music dataset).

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.

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

  1. An IndyKite Project, Application, Application Agent, and credentials provisioned via the Config API.
  2. A Token Introspect configuration that links your external IdP (Auth0) tokens to Person nodes in the IKG.
  3. An MCP Server configuration so AI agents can call IndyKite over MCP.
  4. A music IKG populated with the 6 node types and 13 relationship types listed below.
  5. 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.
  6. 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
Artist94artist-1 (38 Special)
Album1 247album-1
Track14 418track-1
Playlist20playlist-1
Person86 (12 with auth0|... external_ids)person-1 / auth0|69c3ee8cb9ed562744ff9326
Venue24venue-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

  1. Download & import the collection (link at the top of this page) into Postman. The collection's "Variables" tab shows every variable you might need.
  2. 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.
  3. 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.
  4. The chapters below explain each section. Skip ahead if you only care about one product, but the variable chain only works left-to-right.

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.

1

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
PersonUser 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
ArtistMusician or group.name
AlbumReleased by an Artist; contains Tracks.title
TrackIndividual song with audio characteristics used in policies (e.g. loudness gates).title, duration, danceability, energy, loudness, popularity
PlaylistUser-created or system playlist. Has venue approvals and subscriber relationships.name
VenuePlace where tracks are played. Carries thresholds policies compare against.name, min_confidence, min_danceability, max_loudness, max_energy

Relationships

Relationship From → To Used by
CREATEDArtist → Track, Person → PlaylistCatalog ownership / playlist authorship
PART_OFTrack → AlbumCatalog hierarchy
RELEASEDArtist → AlbumCatalog ownership
LIKESPerson → TrackSame-taste matching, popular-tracks queries
FOLLOWSPerson → ArtistSame-artist-followers query
SUBSCRIBED_TOPerson → PlaylistSubscription read / share KBAC
MARRIED_TO / PARTNERS / PARENT_OF / CO_PARENTSPerson ↔ PersonFamily-playlist KBAC, children-likes CIQ
WILL_ATTENDPerson → VenueConcert / venue-attendees / DJ KBAC
APPROVED_FORPlaylist → VenuePlaylist-venue-approval KBAC
PLAYED_ATTrack → VenueVenue-playable-tracks CIQ, track-loudness KBAC

The scenarios you'll author

Three families of authorization questions are answered in later chapters:

  1. KBAC / AuthZEN (Chapters 6 - 7): yes/no decisions like "can person-52 PERFORM at venue-1?" or "can track-7431 PLAY at venue-4?"
  2. ContX IQ - Application as subject (Chapter 8 onwards): system-level reads/writes like "list playable tracks for venue X" run by the _Application identity.
  3. 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.

2

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

LevelWhy it exists
OrganizationTop-level account, holds Service Accounts and Projects.
ProjectIsolated environment with its own IKG.
ApplicationLogical grouping inside a Project. Creates an _Application node so the application can act as a subject.
Application AgentAPI 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
1create projectPOST /configs/v1/projectsproject_gid
2create applicationPOST /configs/v1/applicationsapplication_gid
3create application agentPOST /configs/v1/application-agentsapp_agent_gid
4create application agent credentialsPOST /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.

3

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 introspectPOST /configs/v1/token-introspectstoken_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. Replace these with your own Auth0 tenant values.
  • 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.

4

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

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 configPOST /configs/v1/mcp-serversCaptures mcp_server_gid.
read mcp server configGET /configs/v1/mcp-servers/{{mcp_server_gid}}Optional - inspect the active config.
delete mcp server configDELETE /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.

5

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 nodesPOST /capture/v1/nodes/15 889 nodes across 6 types
upsert relationshipsPOST /capture/v1/relationships31 297 relationships across 13 types

What gets created

Nodes by type:

  • Artist: 94, Album: 1 247, Track: 14 418
  • Playlist: 20, Person: 86, Venue: 24

Relationships by type:

  • Catalog: CREATED (14 439), PART_OF (14 618), RELEASED (1 258)
  • 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 15 889 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 31 297 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.

6

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…
kbacconcert-performerPersonPERFORMVenuesubject is going to the venue and karaoke_confidence ≥ venue.min_confidence
kbac2venue-entry-by-membershipPersonENTERVenuesubject is going to the venue (any WILL_ATTEND)
kbac3flash-mob-recruitmentPersonJOINVenue (Flash-Mob-Recruitment)subject's dance_skill ≥ venue.min_danceability
kbac4track-loudness-checkTrackPLAYVenuetrack is PLAYED_AT and loudness ≥ max_loudness
kbac5dmv-endless-sufferingTrackPLAYVenue (DMV-Waiting-Area)track.energy ≤ venue.max_energy
kbac6playlist-venue-approvalPlaylistFEATUREVenueplaylist is APPROVED_FOR the venue
kbac7share-subscribed-playlistPersonSHAREPlaylistsubject SUBSCRIBED_TO or CREATED the playlist
kbac8family-playlist-accessPersonVIEWPlaylista family member (MARRIED_TO / PARENT_OF / PARTNERS) CREATED the playlist
kbac9corporate-no-enthusiasmTrackPLAYVenue (Corporate-Meeting-Room)track.energy ≤ venue.max_energy
kbac10dj-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.

7

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
EvaluationPerson/person-52Venue/venue-1PERFORMkbac (concert-performer)
Evaluation2Person/person-52Venue/venue-10ENTERkbac2 (venue-entry-by-membership)
Evaluation3Person/person-47Venue/venue-16JOINkbac3 (flash-mob-recruitment)
Evaluation4Track/track-2724Venue/venue-1PLAYkbac4 (track-loudness-check)
Evaluation5Track/track-7431Venue/venue-4PLAYkbac5 (dmv-endless-suffering)
Evaluation6Playlist/playlist-1Venue/venue-1FEATUREkbac6 (playlist-venue-approval)
Evaluation7Person/person-44Playlist/playlist-17SHAREkbac7 (share-subscribed-playlist)
Evaluation8Person/person-1Playlist/playlist-13VIEWkbac8 (family-playlist-access)
Evaluation9Track/track-7431Venue/venue-14PLAYkbac9 (corporate-no-enthusiasm)
Evaluation10Person/person-40Venue/venue-3DJkbac10 (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:<Type>, 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.

8

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 typeUsed byAuth at execute time
_ApplicationSystem-level catalog ops (venue playable tracks, artist catalog, karaoke-ready people, attendance stats).X-IK-ClientKey: {{your_agent_token}} only.
PersonUser-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)

GroupSubjectPolicies (request → name)
Application catalog ops_Applicationciqpolicy (venue-playable-tracks), ciqpolicy2 (artist-catalog), ciqpolicy3 (karaoke-ready-people), ciqpolicy21 (venue-attendance-stats)
Person - own dataPersonciqpolicy4 (own-profile), 5 (own-liked-tracks), 6 (own-playlists), 7 (own-subscriptions), 8 (own-followed-artists), 9 (own-venues), 24 (own-playlist-engagement)
Person - familyPersonciqpolicy10 (family-playlists), 11 (children-likes)
Person - venue contextPersonciqpolicy12 (venue-other-attendees), 13 (venue-playlists), 17 (matching-venues), 18 (liked-tracks-at-venue), 22 (people-in-venue-list), 23 (eligible-venues-broad)
Person - socialPersonciqpolicy14 (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)<-[:LIKES]-(other:Person)
               WHERE subject <> other
               WITH subject, other, COUNT(DISTINCT track) AS sharedTracks
               OPTIONAL MATCH (subject)-[:FOLLOWS]->(artist:Artist)<-[:FOLLOWS]-(other)
               WITH subject, other, sharedTracks,
                    COUNT(DISTINCT artist) AS sharedArtists",
    "filter": [{"operator":"AND","operands":[
       {"attribute":"subject.external_id","operator":"=", "value":"$token.sub"},
       {"attribute":"sharedTracks",       "operator":">=","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.

9

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

PolicyQueries
ciqpolicykq (read), kqb (write), kqc (delete)
ciqpolicy2kq2, kq2b, kq2c, kq2d
ciqpolicy3kq3
ciqpolicy4kq4 (read), kq4b (write)
ciqpolicy5kq5, kq5b, kq5c
ciqpolicy6kq6, kq6b, kq6c, kq6d
ciqpolicy7 - ciqpolicy93 variants each (read/write/delete)
ciqpolicy10 - ciqpolicy111 read each (family / children's likes)
ciqpolicy12kq12 (read venue attendees)
ciqpolicy133 variants (read/write/delete venue playlists)
ciqpolicy14 - ciqpolicy161 read each (social / matching)
ciqpolicy172 (read, write)
ciqpolicy18 - ciqpolicy241 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.

10

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

SubjectHeaders
_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.

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

ParamDefault valueWhy 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_nameShower-Concert-HallFirst Venue in the dataset (venue-1); has 50 PLAYED_AT tracks and 3 attendees.
artist_nameABBASeeded 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 / 1Permissive 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

  1. The platform looks up the query by id and resolves its policy.
  2. It validates that every $param from the policy's filter is present in input_params - missing one returns HTTP 422.
  3. 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).
  4. It runs the cypher with the params plugged in, then projects only the fields listed in allowed_reads.
  5. 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):

GroupExecutesRows
Application catalog (no bearer)ciq exec kq / kqb / kqc50 tracks at Shower-Concert-Hall
ciq exec kq2 / kq2b / kq2c / kq2d9 albums + 112 tracks for ABBA
ciq exec kq33 karaoke-ready persons
ciq exec kq213 attendees of Shower-Concert-Hall
Person, self-onlykq4 / kq4b / kq17 / kq17b / kq231 row (the caller's own profile / properties)
Person, direct edgeskq5/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, kq241 - 5 rows each (Cornelius's seeded engagement)
Two-hop (venue / family)kq10 (family playlists), kq12, kq13/b/c, kq22ROWS
Parent-side querykq11 (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