
Neo4j Aura Agent Skill
- 317 installs
- 101 repo stars
- Updated August 3, 2026
- neo4j-contrib/neo4j-skills
neo4j-aura-agent-skill is an agent skill that creates, configures, and invokes Neo4j Aura GraphRAG agents over AuraDB using schema-driven tool selection and the v2beta1 REST API.
About
neo4j-aura-agent-skill packages a solo-builder workflow for Neo4j Aura Agents—GraphRAG assistants backed by AuraDB. It targets indie developers who already have graph data in Aura and want repeatable agent setup instead of one-off console clicking. The skill walks schema discovery (including vector index detection), maps real graph shape to the three first-class tools, and drives agent lifecycle through authenticated REST calls with prompts and exposure modes. Install when you are wiring RAG or analytics chat into a product and need agents that respect your ontology rather than generic retrieval. Prerequisites are explicit: enabled Generative AI and Aura Agent in the org, project admin rights, client credentials, and org/project IDs from the console. Use uv-synced Python helpers to list agents, create from config, and smoke-test NL queries before you expose MCP or public REST endpoints to end users.
- Fetches AuraDB graph schema and annotates property types for tool design
- Guides tool choice across CypherTemplate, SimilaritySearch, and Text2Cypher from schema and use case
- Creates, lists, and manages Aura Agents through the v2beta1 REST API
- Sets system prompts and visibility (private/public, REST/MCP)
- Invokes agents with natural language and parses structured responses
Neo4j Aura Agent Skill by the numbers
- 317 all-time installs (skills.sh)
- +27 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #2,239 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/neo4j-contrib/neo4j-skills --skill neo4j-aura-agent-skillAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 317 |
|---|---|
| repo stars | ★ 101 |
| Security audit | 3 / 3 scanners passed |
| Last updated | August 3, 2026 |
| Repository | neo4j-contrib/neo4j-skills ↗ |
What it does
Stand up and operate Neo4j Aura Agents with GraphRAG tools over an AuraDB knowledge graph via REST scripts.
Who is it for?
Best when you're shipping graph-backed AI features and already run AuraDB and hold Aura API credentials.
Skip if: Skip if you're without AuraDB data loaded, without Aura Agent enabled in org settings, or and only need local Neo4j Desktop without cloud agents.
When should I use this skill?
You need to create or manage Neo4j Aura Agents on an existing AuraDB knowledge graph with REST or MCP exposure.
What you get
You get scripted schema fetch, tool-matched agent configs, and working natural-language invocations ready to hook into your app or MCP surface.
- Annotated graph schema output for tool design
- Configured Aura Agent with system prompt and visibility settings
- Validated natural-language query responses from agent invocation
By the numbers
- Three guided Aura Agent tool types: CypherTemplate, SimilaritySearch, Text2Cypher
- Python 3.13+ project with uv sync and documented manage_agent.py / fetch_schema.py scripts
Files
When to Use
- Creating or configuring an Aura Agent on an existing AuraDB instance
- Adding/updating tools (CypherTemplate, SimilaritySearch, Text2Cypher) to an agent
- Deploying an agent for external access (REST API endpoint or MCP server)
- Invoking an agent with natural language queries via REST API
- Listing, reading, or deleting existing agents in a project
When NOT to Use
- Creating/managing AuraDB instances →
neo4j-aura-provisioning-skill - Creating vector indexes →
neo4j-vector-index-skill - Running Cypher directly →
neo4j-cypher-skill - Building Aura Graph Analytics sessions →
neo4j-aura-graph-analytics-skill
---
What are Aura Agents
GraphRAG agents on top of AuraDB — answer natural language questions via three tool types:
- CypherTemplate — parameterized queries for predictable lookups
- SimilaritySearch — vector similarity search over a VECTOR index
- Text2Cypher — natural language → Cypher for aggregations and discovery
Expose your graph via natural language to users or apps without application code. Accessible as REST or MCP endpoint; single- and multi-turn. For full Cypher control, low-latency lookups, or direct writes — use neo4j-cypher-skill instead.
---
Prerequisites
- Running AuraDB instance with knowledge graph loaded
- "Generative AI assistance" enabled in Organization settings
- "Aura Agent" toggled on in the project
- "Tool authentication" enabled at project/Security level
- Project admin access
AURA_CLIENT_IDandAURA_CLIENT_SECRETfrom console.neo4j.io → Account Settings → API CredentialsAURA_ORG_ID,AURA_PROJECT_ID— see Step 2;AURA_INSTANCE_ID— resolved interactively in Step 2 if not already set- Python env:
uv syncin skill directory (orpip install neo4j neo4j-graphrag requests python-dotenv) .envandschema.jsonin.gitignore
---
Step 1 — Verify Auth
Manual credential verification only — scripts call get_token() internally.
TOKEN=$(curl -s --request POST 'https://api.neo4j.io/oauth/token' \
--user "${AURA_CLIENT_ID}:${AURA_CLIENT_SECRET}" \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
| jq -r '.access_token')
echo "Token: ${TOKEN:0:20}..."If blank token: verify AURA_CLIENT_ID/AURA_CLIENT_SECRET in .env. Stop and report. Token TTL: 3600 s. Re-run on 401/403.
---
Step 2 — Resolve Organization & Project IDs
From console URL (fastest): open console.neo4j.io → navigate to a project. URL pattern: /organizations/{AURA_ORG_ID}/projects/{AURA_PROJECT_ID}
Programmatic fallback:
curl -s https://api.neo4j.io/v1/tenants \
-H "Authorization: Bearer $TOKEN" | jq '.data[] | {id, name}'
# tenant id maps to AURA_PROJECT_IDSet in .env:
AURA_ORG_ID=<organization-id>
AURA_PROJECT_ID=<project-id>Check `AURA_INSTANCE_ID` — if it is already set in .env, skip the rest of this step.
If not set, list available instances and ask the user to choose:
curl -s "https://api.neo4j.io/v1/instances?tenantId=${AURA_PROJECT_ID}" \
-H "Authorization: Bearer $TOKEN" \
| jq '.data[] | {id, name, status, region, type}'Show output to user. Ask: "Which instance should the agent connect to?" Then write to .env:
AURA_INSTANCE_ID=<chosen-instance-id>
NEO4J_URI=neo4j+s://<chosen-instance-id>.databases.neo4j.ioIf the list is empty: no AuraDB instances exist in this project — an Aura Agent cannot be created without one. Stop and report. If 401: re-run Step 1. If 404: verify AURA_PROJECT_ID. Stop and report.
---
Step 3 — List Existing Agents
uv run python3 scripts/manage_agent.py list # Linux/macOS
uv run python scripts\manage_agent.py list # WindowsOutput: agent IDs, names, enabled status, endpoint URLs.
If 401: re-run Step 1. If 404: verify AURA_ORG_ID/AURA_PROJECT_ID. Stop and report.
---
Step 4 — Fetch Graph Schema
Requires NEO4J_URI, NEO4J_USERNAME, NEO4J_PASSWORD in .env.
uv run python3 scripts/fetch_schema.py # Linux/macOS
uv run python scripts\fetch_schema.py # WindowsSaves schema.json. Output: node/rel-type counts, node labels + typed properties (with Aura data_type), relationship patterns, VECTOR indexes.
Data gate — script exits with error and does NOT write schema.json if:
- fewer than 2 nodes, OR
- zero relationship types
If gate fails: load data into the database before proceeding. Stop and report. If ServiceUnavailable: check NEO4J_URI uses neo4j+s://; instance must be running. Stop and report. If neo4j-graphrag not found: uv add neo4j-graphrag. Stop and report.
Read schema.json before Step 5.
---
Step 5 — Discover Use Cases
Before designing tools, read references/authoring-guide.md.
Ask the user these questions. Do NOT guess tool types or parameters.
1. "What questions should this agent answer?" 2. "Which nodes or relationships matter most?" — match against schema.json → node_props 3. "Do users search by a specific property value?" → CypherTemplate 4. "Any counting, grouping, or date-range questions?" → Text2Cypher 5. "Search for semantically similar text?" → check schema.json → metadata → vector_index
- No VECTOR index found: inform user; skip SimilaritySearch; delegate to
neo4j-vector-index-skillfirst - VECTOR index found: ask the user — "Which embedding provider and model should be used? What output dimension?" See supported models in
references/REFERENCE.md → Embedding Provider Options. Do NOT guess or default.
Tool selection:
| Use Case | Tool |
|---|---|
| Lookup by specific property value | cypherTemplate |
| Semantic text search | similaritySearch |
| Aggregation, counting, open-ended | text2cypher |
CypherTemplate parameters: for each parameter, read aura_data_type from schema.json → node_props or rel_props and use it as data_type. If the property has low_cardinality: true, the parameter description MUST list the valid values — copy them from the values array in schema.json. Example: "description": "Agreement type to filter by. Valid values: \"Distributor Agreement\", \"License Agreement\", \"NDA\"". Properties with has_fulltext_index: true are especially likely to be filter targets and must include valid values when low cardinality.
SimilaritySearch configuration — ask the user for all three before drafting the tool config:
| Field | What to ask | Source |
|---|---|---|
provider | "openai" or "vertexai"? | User confirms |
model | Which model? | User picks from references/REFERENCE.md → Embedding Provider Options |
dimension | What output dimension? | Required if model is configurable (see table); fixed models use the table value |
index: use name from schema.json → metadata → vector_index where state = ONLINE. dimension must match vector.dimensions in the same index entry.
Signals inventory: for each label or relationship that appears in a tool or the user's stated questions, write a signal block in the system prompt. See references/authoring-guide.md → Signals inventory for the template and rules.
Draft config JSON → show to user for review → confirm → proceed to Step 6.
---
Step 6 — Create Agent
Minimum required config:
{
"name": "My Agent",
"description": "Answers questions about the graph",
"dbid": "<AURA_INSTANCE_ID>",
"is_private": false,
"tools": [
{
"type": "text2cypher",
"name": "Query Graph",
"description": "Translates natural language questions into Cypher queries"
}
]
}Show config to user and confirm before running:
uv run python3 scripts/manage_agent.py create --config agent-config.jsonResponse includes id (save as AURA_AGENT_ID), endpoint_link, mcp_endpoint_link.
---
Step 7 — Invoke Agent (Test)
uv run python3 scripts/invoke_agent.py --agent-id "$AURA_AGENT_ID" "What can you help me with?"--raw prints full JSON including reasoning chain and token usage.
Direct curl (uses token from Step 1):
curl -s -X POST \
"https://api.neo4j.io/v2beta1/organizations/${AURA_ORG_ID}/projects/${AURA_PROJECT_ID}/agents/${AURA_AGENT_ID}/invoke" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"input": "What can you help me with?"}'---
Step 8 — Update Agent (Partial PATCH)
Create patch JSON with only the fields to change:
{ "system_prompt": "Updated instructions.", "is_mcp_enabled": true }Show to user and confirm before running:
uv run python3 scripts/manage_agent.py update --agent-id "$AURA_AGENT_ID" --config patch.json---
Step 9 — Delete Agent
IRREVERSIBLE. Configuration permanently removed.
Show to user and wait for explicit confirmation before running:
uv run python3 scripts/manage_agent.py delete --agent-id "$AURA_AGENT_ID"Returns 202 Accepted.
---
Tool Configuration
CypherTemplate
Pre-defined parameterized queries for repeated, predictable lookups.
{
"type": "cypherTemplate",
"name": "<descriptive name>",
"description": "<what it looks up and when to use it>",
"enabled": true,
"config": {
"template": "MATCH (n:Label {prop: $param}) RETURN n",
"parameters": [
{
"name": "param",
"data_type": "<string|integer|number|boolean — from schema.json aura_data_type>",
"description": "<what the parameter represents. If low_cardinality=true in schema.json, append: Valid values: \"val1\", \"val2\", ...>"
}
]
}
}Low-cardinality rule: if schema.json → node_props[Label][prop].low_cardinality is true, the description field must end with the exact values from schema.json → node_props[Label][prop].values. This applies to relationship properties in rel_props too.
SimilaritySearch
Requires a VECTOR index (state = ONLINE). Get index name from schema.json → metadata → vector_index.
{
"type": "similaritySearch",
"name": "<descriptive name>",
"description": "<what text it searches and when to use it>",
"enabled": true,
"config": {
"provider": "openai",
"model": "text-embedding-3-small",
"index": "<name from schema.json metadata.vector_index[state=ONLINE].name>",
"top_k": 5,
"dimension": "<vector.dimensions from schema.json metadata.vector_index options.indexConfig>",
"post_processing_cypher": "<optional: Cypher to enrich similarity results with related nodes>"
}
}provider/model combinations: see references/REFERENCE.md.
Text2Cypher
Natural language → Cypher. Use as fallback for aggregation and discovery.
{
"type": "text2cypher",
"name": "<descriptive name>",
"description": "<what questions it handles — and explicitly what it should NOT handle>",
"enabled": true
}---
Common Errors
| Error | Cause | Fix |
|---|---|---|
401 Unauthorized | Token expired | Re-run Step 1 |
403 Forbidden on create | Not a project admin | Request admin access |
400 Bad Request | Invalid tool config or missing required field | Check type spelling: cypherTemplate, similaritySearch, text2cypher |
404 Not Found | Wrong org/project/agent ID | Re-run list to verify IDs |
400 on create with SimilaritySearch | Vector index missing | Create index first — use neo4j-vector-index-skill |
| Agent returns no results | top_k too low or index empty | Increase top_k; verify index is populated |
---
Scripts
All scripts load credentials from .env automatically. Run with uv run python3 <script>.
| Script | Purpose |
|---|---|
scripts/fetch_schema.py | Fetch graph schema from AuraDB; save to schema.json |
scripts/manage_agent.py | CRUD: list, create, get, update, delete agents |
scripts/invoke_agent.py | Send a natural language query to an agent |
fetch_schema.py parameters:
| Parameter | Type | Required | Default |
|---|---|---|---|
NEO4J_URI | env | Yes | — |
NEO4J_USERNAME | env | No | neo4j |
NEO4J_PASSWORD | env | Yes | — |
NEO4J_DATABASE | env | No | neo4j |
manage_agent.py parameters:
| Parameter | Type | Required | Env fallback |
|---|---|---|---|
AURA_CLIENT_ID | env | Yes | — |
AURA_CLIENT_SECRET | env | Yes | — |
--org-id | arg | No | AURA_ORG_ID |
--project-id | arg | No | AURA_PROJECT_ID |
--agent-id | arg | get/update/delete | AURA_AGENT_ID |
--config | arg | create/update | — |
invoke_agent.py parameters:
| Parameter | Type | Required | Env fallback |
|---|---|---|---|
AURA_CLIENT_ID | env | Yes | — |
AURA_CLIENT_SECRET | env | Yes | — |
--org-id | arg | No | AURA_ORG_ID |
--project-id | arg | No | AURA_PROJECT_ID |
--agent-id | arg | Yes | AURA_AGENT_ID |
query | positional | Yes | — |
--raw | flag | No | — |
---
Checklist
- [ ] AuraDB instance
running, knowledge graph loaded - [ ] "Generative AI assistance" + "Aura Agent" enabled in org/project settings
- [ ]
.envpopulated:AURA_CLIENT_ID,AURA_CLIENT_SECRET,AURA_ORG_ID,AURA_PROJECT_ID,AURA_INSTANCE_ID,NEO4J_URI,NEO4J_PASSWORD - [ ]
.envandschema.jsonin.gitignore - [ ] Auth verified (Step 1)
- [ ] Org/Project IDs confirmed (Step 2)
- [ ] API connectivity confirmed via
list(Step 3) - [ ]
schema.jsonfetched and reviewed (Step 4) — data gate passed (≥2 nodes, ≥1 rel type) - [ ] Use cases confirmed with user (Step 5)
- [ ] CypherTemplate
data_typetaken fromschema.json aura_data_type - [ ] SimilaritySearch
indexfromschema.json metadata.vector_index(state=ONLINE) - [ ] Agent config shown to user and confirmed (Step 6)
- [ ] Required fields present:
name,description,dbid,is_private,tools(min 1) - [ ]
AURA_AGENT_IDsaved from create response - [ ] Agent invoked and response verified (Step 7)
- [ ] Update/Delete confirmed by user before execution
3.13
[project]
name = "neo4j-aura-agent-skill"
version = "0.1.0"
description = "Scripts for creating and managing Neo4j Aura Agents via the v2beta1 REST API"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"neo4j>=6.1.0",
"neo4j-graphrag>=1.15.0",
"python-dotenv>=1.2.2",
"requests>=2.33.1",
]
neo4j-aura-agent-skill
Create, configure, and invoke Neo4j Aura Agents — GraphRAG agents backed by an AuraDB knowledge graph.
What this skill does
- Fetches the graph schema from AuraDB and annotates property types for tool design
- Guides tool selection (CypherTemplate, SimilaritySearch, Text2Cypher) based on use cases and schema
- Creates and manages Aura Agents via the v2beta1 REST API
- Sets system prompts and agent visibility (private/public, REST/MCP)
- Invokes agents with natural language queries and parses responses
Requirements
- Running AuraDB instance with knowledge graph loaded
- "Generative AI assistance" + "Aura Agent" enabled in Aura org settings
- Project admin access
AURA_CLIENT_IDandAURA_CLIENT_SECRETfrom console.neo4j.io → API Credentials- Organization ID and Project ID from the Aura console URL
Install dependencies
uv syncQuick start
# Fetch graph schema and detect vector indexes
uv run python3 scripts/fetch_schema.py
# List existing agents
uv run python3 scripts/manage_agent.py list
# Create an agent from a config file
uv run python3 scripts/manage_agent.py create --config agent-config.json
# Invoke the agent
uv run python3 scripts/invoke_agent.py --agent-id "$AURA_AGENT_ID" "What can you help me with?"Files
| File | Purpose |
|---|---|
SKILL.md | Agent-readable operational playbook |
scripts/fetch_schema.py | Fetch graph schema from AuraDB; save to schema.json |
scripts/manage_agent.py | CRUD operations (list/create/get/update/delete) |
scripts/invoke_agent.py | Send queries to a deployed agent |
references/REFERENCE.md | Full API schema, embedding providers, response formats |
Aura Agent Authoring Guide
Best practices for system prompts, tool names, tool descriptions, and parameter descriptions. The agent's LLM reads all of these at inference time to decide which tool to call and how to answer. Quality here determines routing accuracy and answer quality.
---
System Prompt
Purpose
Defines the agent's identity, scope, and behavior constraints. Supplements — does not replace — tool descriptions.
Rules
Role sentence — one sentence: domain + expertise level.
✅ "You are a specialist assistant for commercial contract analysis using a knowledge graph."
❌ "You are a helpful AI assistant."Supported use cases — bullet list of what it can answer. Keeps LLM from fabricating capabilities.
✅ "You can: look up agreements by party or type, find similar clause language, identify contracts expiring within a date range."
❌ (omitting this — LLM will guess its own scope)Explicit boundaries — state what it cannot do; do not rely on tool absence alone.
✅ "You cannot modify data, generate contract drafts, or access documents outside the knowledge graph."
❌ (omitting this — LLM may attempt tasks outside its tools)Tool preference order — tell it which tool to try first for specific lookups. Without this, LLM defaults to Text2Cypher for everything.
✅ "For lookups by a known ID or property value, prefer CypherTemplate tools over the Text2Cypher tool."Uncertainty handling — tell it to say so when it cannot answer. Prevents hallucination.
✅ "If you cannot find an answer using the available tools, say so explicitly. Do not guess."Output format — specify structure when the use case demands it.
✅ "Always include the source node ID or property in your answer so users can verify."
✅ "For comparisons across multiple items, use a table."Citation rule — tell it to reference graph data, not LLM knowledge.
✅ "Base all answers on data retrieved from tools. Do not use your training knowledge about the domain."Explain mode — when the user asks to explain an answer, the agent should go beyond restating results. Include this block in your system prompt:
✅ "If the user asks you to explain your answer:
1. Describe how you determined the result — which tool you used, which nodes or relationships were traversed, and why.
2. Suggest ways the user could ask a more precise or targeted question to get better results.
3. If a CypherTemplate tool would answer this question more reliably than Text2Cypher, propose the template — include the Cypher query, parameter names, and data types.
4. If a change to the data model (new property, index, or relationship type) would make this query faster or more accurate, describe it."This pattern surfaces agent reasoning and turns every answer into an opportunity to improve the tools and data model over time.
Signals inventory — include a signals section in the prompt for each key label and relationship type relevant to the use cases. Without it, the agent sees node labels and properties but must infer their semantic meaning, valid values, and routing logic from the schema alone. A signals inventory makes that knowledge explicit.
Each signal entry should state:
- What the signal measures (semantic meaning)
- Where it lives in the graph (label, relationship pattern, property)
- Valid values (critical for filtering — especially low-cardinality properties)
- When to use it (which question types or ranking/filtering scenarios call for it)
Generate this section during Step 5 using the use case discussion and schema.json. For every label or relationship that appears in a tool or in the user's questions, write a signal block.
Template:
## <Domain> Signals
**<Signal Name>** — `(<StartLabel>)-[r:<REL_TYPE>]->(<EndLabel>)` (or `(<Label>)` for node properties)
- Property: `r.<propertyName>` (or `n.<propertyName>`)
- Values: `'value1'` | `'value2'` | `'value3'`
- When to use: <question types or ranking/filtering scenarios>Example — recruiting domain:
## Candidate Quality Signals
**Skill Proficiency** — `(Person)-[hs:HAS_SKILL]->(Skill)`
- Property: `hs.proficiencyLevel`
- Values: `'expert'` | `'advanced'` | `'intermediate'` | `'beginner'`
- When to use: Ranking candidates, assessing readiness, computing skill depth scores
**Employment Status** — `(Person)`
- Property: `n.status`
- Values: `'active'` | `'inactive'` | `'pending'`
- When to use: Filtering to current employees or open candidates onlyRules:
- Include only labels/relationships that appear in the agent's tools or the user's stated questions — do not enumerate the entire schema
- Always include valid values for low-cardinality properties (copy from
schema.json → values) - If a property has
has_fulltext_index: true, note that in the "When to use" line: "Supports full-text search" - Keep each block to 4–6 lines; signal sections longer than ~300 words start to dilute the prompt budget
Anti-Patterns
| Anti-pattern | Problem |
|---|---|
| "Answer any question about the graph" | No scope → LLM uses Text2Cypher for everything |
| Prompt that restates tool descriptions | Redundant tokens; contradictions if they diverge |
| No uncertainty clause | LLM fabricates answers when tools return nothing |
| Very long prompt (>500 words) | Dilutes the constraints; LLM ignores later rules |
---
Tool Names
Format: Verb + specific object. Max ~5 words.
| ✅ Good | ❌ Bad | Why |
|---|---|---|
Get Agreement by ID | Agreement Lookup | No verb → ambiguous trigger |
Find Agreements Expiring This Year | Date Filter | Too vague |
Count Agreements by Type | Aggregation Tool | Doesn't say what it aggregates |
Find Similar Clause Text | Semantic Search | No domain context |
Summarize Graph Statistics | Text2Cypher Tool | Never name a tool after its mechanism |
Do NOT include the word "Tool" in a name — redundant and wastes the LLM's routing signal.
---
Tool Descriptions
The description is the primary signal the LLM uses to select a tool. It must answer three questions: 1. When to use this tool (trigger condition) 2. What it returns 3. When NOT to use it (prevents wrong selection when tools overlap)
Structure
Use [trigger condition]. Returns [what the output contains].
Do NOT use [exclusion condition] — use [alternative tool name] instead.CypherTemplate
✅ "Use when the user asks for a specific agreement by its contract ID.
Returns party names, agreement type, effective date, and expiration date.
Do NOT use for aggregations or open-ended discovery — use the aggregation tool instead."
❌ "Runs MATCH (a:Agreement {contract_id: $contract_id}) and returns agreement data."
(describes the Cypher, not the use case — LLM cannot route from this)Rules:
- Describe the question pattern, not the query mechanics
- Name the key parameter in plain language ("by its contract ID", "by city name")
- If a filter parameter has a fixed set of valid values, state that in the description too:
"Valid filter values are: 'Type A', 'Type B', 'Type C'"
SimilaritySearch
✅ "Use when the user wants to find nodes whose text is semantically similar to a phrase or sentence.
Returns the top-K most similar results ranked by embedding distance.
Do NOT use for exact property matches — use a CypherTemplate tool for those."
❌ "Performs vector search on the excerpt_embedding index."
(mechanism, not use case)Rules:
- Say what kind of text is embedded ("full clause text", "product descriptions", "support ticket summaries")
- Make clear this is approximate/semantic, not exact
- State the exclusion: exact-match queries belong in CypherTemplate
Text2Cypher
Text2Cypher is the most flexible tool — and the one most likely to be overused. The description must tightly bound its scope.
✅ "Use for open-ended discovery and aggregation: counting nodes, grouping by category,
finding patterns not covered by other tools. Example questions: 'How many agreements
exist per type?', 'Which organizations appear most frequently?'.
Do NOT use for lookups by a known ID or property value — use the specific CypherTemplate
tools for those. Do NOT use for similarity search — use the similarity search tool."
❌ "Translates natural language to Cypher."
(says nothing about scope — LLM uses it for everything)Rules:
- List 2–3 example questions it handles well — LLM uses these as routing anchors
- Always have at least two explicit exclusions naming the alternative tool
- Put this tool last in the
toolsarray — LLM tries tools in order; Text2Cypher is the fallback
---
CypherTemplate Parameter Descriptions
Parameter descriptions are shown to the LLM when it needs to fill in a parameter value from the user's message. A poor description causes wrong values or failed extractions.
Rules
Always state what the parameter represents:
✅ "description": "The unique contract identifier, e.g. 'CUAD-001'"
❌ "description": "id"Low-cardinality properties — always list valid values:
When schema.json → node_props[Label][prop].low_cardinality is true, copy the values array verbatim into the description. The LLM will normalize user input to a valid value.
✅ "description": "Clause type to filter by. Valid values: \"Anti-Assignment\", \"Exclusivity\", \"Governing Law\", \"IP Ownership Assignment\", \"License Grant\", \"Non-Compete\", \"Termination For Convenience\""
❌ "description": "The type of clause"
(LLM guesses — may pass a value that matches no nodes)Date parameters — include format:
✅ "description": "Effective date to filter from. Format: YYYY-MM-DD (e.g. '2023-01-01')"
❌ "description": "Start date"ID parameters — say where the user gets the value:
✅ "description": "Contract ID from a prior 'Get Agreement' result or from the user's request"
❌ "description": "Contract ID"Full-text indexed properties (has_fulltext_index: true in schema.json) — especially important to list valid values; the full-text index implies this property is a primary filter target.
Anti-Patterns
| Anti-pattern | Problem |
|---|---|
Description is the param name only ("id", "type") | LLM cannot extract correctly from varied user phrasing |
| No valid values on low-cardinality property | LLM invents values; Cypher returns nothing |
| Missing format on date/time param | LLM passes wrong format; Cypher fails silently |
| Description longer than 2000 chars | API truncates; valid values may be cut |
---
Tool Ordering
The LLM considers tools in array order when multiple tools could answer a question.
Recommended order: 1. Most specific CypherTemplate tools (exact lookups) 2. Broader CypherTemplate tools (filtered searches) 3. SimilaritySearch (if present) 4. Text2Cypher (always last — catches what others can't)
"tools": [
{ "type": "cypherTemplate", "name": "Get Agreement by ID", ... },
{ "type": "cypherTemplate", "name": "Get Agreements by Clause Type", ... },
{ "type": "similaritySearch", "name": "Find Similar Clause Text", ... },
{ "type": "text2cypher", "name": "Discover and Aggregate", ... }
]---
Checklist Before Creating
- [ ] System prompt has: role sentence, supported use cases, explicit boundaries, tool preference order, uncertainty clause
- [ ] System prompt is ≤500 words
- [ ] Every tool name is a verb + specific object (no "Tool" suffix)
- [ ] Every tool description answers: when to use, what it returns, when NOT to use
- [ ] Every CypherTemplate tool names at least one exclusion with an alternative
- [ ] Text2Cypher is last in the tools array
- [ ] Text2Cypher description has ≥2 explicit exclusions
- [ ] Every parameter on a low-cardinality property lists all valid values verbatim
- [ ] Date/time parameters include format string
- [ ] ID parameters say where the user obtains the value
Aura Agent API Reference (v2beta1)
Full API specification: https://neo4j.com/docs/aura/platform/api/specification/aura_api_spec_v2beta1.yaml
Base URL
https://api.neo4j.io/v2beta1Auth
All requests require a Bearer token obtained via:
POST https://api.neo4j.io/oauth/token
Authorization: Basic base64(CLIENT_ID:CLIENT_SECRET)
Content-Type: application/x-www-form-urlencoded
Body: grant_type=client_credentialsToken TTL: 3600 s. On 401/403: re-authenticate.
---
Agent Endpoints
| Method | Path | Description |
|---|---|---|
GET | /organizations/{orgId}/projects/{projectId}/agents | List agents |
POST | /organizations/{orgId}/projects/{projectId}/agents | Create agent |
GET | /organizations/{orgId}/projects/{projectId}/agents/{agentId} | Get agent |
PUT | /organizations/{orgId}/projects/{projectId}/agents/{agentId} | Full replace |
PATCH | /organizations/{orgId}/projects/{projectId}/agents/{agentId} | Partial update |
DELETE | /organizations/{orgId}/projects/{projectId}/agents/{agentId} | Delete agent |
POST | /organizations/{orgId}/projects/{projectId}/agents/{agentId}/invoke | Invoke agent |
---
CreateAgentRequest Schema
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | ✅ | Max 100 chars |
description | string | ✅ | |
dbid | string | ✅ | AuraDB instance ID |
is_private | boolean | ✅ | false = accessible to project members; true = creator only |
tools | array | ✅ | Min 1 tool; see Tool Schemas |
system_prompt | string | ❌ | Custom instructions for the LLM |
is_mcp_enabled | boolean | ❌ | Enable MCP server endpoint |
enabled | boolean | ❌ | Default: true |
PatchAgentRequest Schema
All fields optional: name, description, system_prompt, dbid, is_private, is_mcp_enabled, tools, enabled.
---
Tool Schemas
CypherTemplate
{
"type": "cypherTemplate",
"name": "string (required)",
"description": "string",
"enabled": true,
"config": {
"template": "MATCH (n {id: $id}) RETURN n",
"parameters": [
{
"name": "id",
"data_type": "string",
"description": "Node ID to look up (max 2000 chars)"
}
]
}
}data_type enum: string | number | boolean | integer
SimilaritySearch
{
"type": "similaritySearch",
"name": "string (required)",
"description": "string",
"enabled": true,
"config": {
"provider": "openai",
"model": "text-embedding-3-small",
"index": "vector_index_name",
"top_k": 5,
"dimension": 1536,
"post_processing_cypher": "OPTIONAL MATCH (node)<-[:HAS_EXCERPT]-(cc) RETURN node, score, cc"
}
}dimension must match the vector index dimension (vector.dimensions in schema.json → metadata.vector_index[].options.indexConfig). post_processing_cypher is optional — use it to traverse from matched nodes to related context nodes.
Text2Cypher
{
"type": "text2cypher",
"name": "string (required)",
"description": "string",
"enabled": true
}---
Embedding Provider Options
Always confirm provider, model, and dimension with the user before writing a SimilaritySearch tool config. Do not default.
OpenAI (provider: "openai")
| Model | Default Dimension | Configurable |
|---|---|---|
text-embedding-3-small | 1536 | Yes |
text-embedding-3-large | 3072 | Yes |
text-embedding-ada-002 | 1536 | No — always 1536 |
Vertex AI (provider: "vertexai")
| Model | Default Dimension | Configurable | Notes |
|---|---|---|---|
gemini-embedding-001 | 3072 | Yes (1–3072) | General purpose |
text-embedding-005 | 768 | Yes (1–768) | Optimized for retrieval |
text-multilingual-embedding-002 | 768 | Yes (1–768) | Multilingual |
---
AgentDetails Response Schema
{
"id": "string",
"name": "string",
"description": "string",
"system_prompt": "string",
"dbid": "string",
"project_id": "string",
"organization_id": "string",
"created_by": "string",
"created_at": "datetime",
"updated_at": "datetime",
"is_private": false,
"is_mcp_enabled": false,
"enabled": true,
"endpoint_link": "https://api.neo4j.io/v2beta1/organizations/.../invoke",
"mcp_endpoint_link": "https://api.neo4j.io/v2beta1/organizations/.../mcp",
"tools": [...]
}---
InvokeAgentRequest
{ "input": "How many contracts are in the database?" }Or multi-turn:
{
"input": [
{ "role": "user", "content": "What contracts does Acme Corp have?" }
]
}Note: Aura Agent does NOT store conversation history between requests. Include prior context in input array for multi-turn.
InvokeAgentResponse
{
"id": "string",
"type": "message",
"role": "assistant",
"content": [
{ "type": "text", "text": "There are 510 contracts in the database." },
{ "type": "tool_use", "name": "Aggregation and Discovery Tool", "input": {...} },
{ "type": "tool_result", "content": [...] }
],
"end_reason": "end_turn",
"status": "completed",
"usage": {
"request_tokens": 245,
"response_tokens": 87,
"total_tokens": 332
}
}type enum: message | error
Error response:
{
"type": "error",
"error": {
"message": "Agent not found",
"type": "not_found",
"status_code": 404
}
}---
HTTP Status Codes
| Code | Meaning |
|---|---|
200 | Success (list, get, update, invoke) |
201 | Agent created |
202 | Delete accepted |
400 | Invalid request body or parameters |
401 | Token expired or missing |
403 | Insufficient permissions (not project admin) |
404 | Agent/project/org not found |
500 | Server error — retry with backoff |
---
Neo4j → Aura Agent Type Mapping
Used when reading schema.json to set data_type in CypherTemplate parameters. fetch_schema.py pre-computes the aura_data_type field on every property.
| Neo4j Property Type | Aura data_type | Notes |
|---|---|---|
STRING | string | |
INTEGER | integer | |
LONG | integer | Neo4j internal; maps to integer |
FLOAT | number | |
DOUBLE | number | |
BOOLEAN | boolean | |
DATE | string | Pass as ISO 8601: "2024-01-15" |
DATE_TIME | string | Pass as ISO 8601: "2024-01-15T10:00:00Z" |
LOCAL_DATE_TIME | string | Pass as "2024-01-15T10:00:00" |
LOCAL_TIME | string | Pass as "10:00:00" |
TIME | string | Pass as "10:00:00+01:00" |
DURATION | string | Pass as ISO 8601 duration: "P1Y2M" |
POINT | string | Pass as WKT: "POINT(1.0 2.0)" |
LIST | string | Serialize as JSON string |
MAP | string | Serialize as JSON string |
ANY | string | Fallback |
---
schema.json Structure
Output of scripts/fetch_schema.py. Extends get_structured_schema() with type annotations, cardinality enrichment, and index metadata.
{
"node_props": {
"Agreement": [
{
"property": "contract_id", "type": "INTEGER", "aura_data_type": "integer",
"low_cardinality": false, "has_fulltext_index": false
},
{
"property": "agreement_type", "type": "STRING", "aura_data_type": "string",
"low_cardinality": true,
"values": ["Distributor Agreement", "License Agreement", "NDA"],
"has_fulltext_index": false
}
],
"ContractClause": [
{
"property": "type", "type": "STRING", "aura_data_type": "string",
"low_cardinality": true,
"values": ["Anti-Assignment", "Exclusivity", "Governing Law"],
"has_fulltext_index": true
}
]
},
"rel_props": {
"IS_PARTY_TO": [
{
"property": "role", "type": "STRING", "aura_data_type": "string",
"low_cardinality": true, "values": ["Buyer", "Seller"],
"has_fulltext_index": false
}
]
},
"relationships": [
{"start": "Agreement", "type": "HAS_CLAUSE", "end": "ContractClause"},
{"start": "Organization", "type": "IS_PARTY_TO", "end": "Agreement"}
],
"metadata": {
"node_count": 1240,
"constraint": [...],
"index": [...],
"vector_index": [
{
"name": "excerpt_embedding",
"type": "VECTOR",
"labelsOrTypes": ["Excerpt"],
"properties": ["embedding"],
"state": "ONLINE",
"options": {
"indexConfig": {"vector.dimensions": 3072},
"indexProvider": "vector-2.0"
}
}
],
"fulltext_index": [
{
"name": "clause_type_fulltext",
"type": "FULLTEXT",
"labelsOrTypes": ["ContractClause"],
"properties": ["type"],
"state": "ONLINE"
}
]
}
}Key fields:
aura_data_type— Aura-compatibledata_typevalue; use directly in CypherTemplate parameterslow_cardinality—trueif ≤50 distinct values;descriptionMUST list valid values when truevalues— sorted list of distinct values; only present whenlow_cardinality: truehas_fulltext_index—trueif a FULLTEXT index covers this property; priority filter targetmetadata.vector_index— usable in SimilaritySearch; filter bystate == "ONLINE"; usenameasindex,options.indexConfig["vector.dimensions"]asdimensionmetadata.fulltext_index— cross-referenced to sethas_fulltext_indexon matching properties
---
Geography Constraint
All Aura Agents run in europe-west1 (Belgium, GCP). Regardless of AuraDB instance region, agent inference happens in EU. Consider data residency requirements before storing PII in agent queries.
---
External Access (REST + MCP)
Enable via is_private: false and optionally is_mcp_enabled: true in the agent config.
External endpoint URL format:
https://api.neo4j.io/v2beta1/organizations/{orgId}/projects/{projectId}/agents/{agentId}/invokeMCP server URL format:
https://api.neo4j.io/v2beta1/organizations/{orgId}/projects/{projectId}/agents/{agentId}/mcpBoth require the same OAuth2 bearer token. Use Aura API credentials (AURA_CLIENT_ID / AURA_CLIENT_SECRET) — not Neo4j database credentials.
#!/usr/bin/env python3
"""Fetch the AuraDB graph schema and save it to schema.json.
Uses neo4j-graphrag get_structured_schema() to retrieve node properties,
relationship patterns, and index metadata. Enriches each STRING property with:
- aura_data_type: Aura-compatible data_type value
- low_cardinality: True if ≤ CARDINALITY_THRESHOLD distinct values exist
- values: sorted list of distinct values (only when low_cardinality=True)
- has_fulltext_index: True if a FULLTEXT index covers this property
Output: schema.json with keys: node_props, rel_props, relationships, metadata
"""
import json
import os
import sys
from pathlib import Path
try:
from dotenv import load_dotenv
load_dotenv(Path(__file__).parent.parent / ".env")
except ImportError:
pass
try:
import neo4j
except ImportError:
sys.exit("neo4j package not found — run: uv add neo4j")
try:
from neo4j_graphrag.schema import get_structured_schema
except ImportError:
sys.exit("neo4j-graphrag not found — run: uv add neo4j-graphrag")
OUTPUT_PATH = Path(__file__).parent.parent / "schema.json"
# Properties with more distinct values than this are not enumerated
CARDINALITY_THRESHOLD = 50
NEO4J_TYPE_TO_AURA = {
"STRING": "string",
"INTEGER": "integer",
"LONG": "integer",
"FLOAT": "number",
"DOUBLE": "number",
"BOOLEAN": "boolean",
"DATE": "string",
"DATE_TIME": "string",
"LOCAL_DATE_TIME": "string",
"LOCAL_TIME": "string",
"TIME": "string",
"DURATION": "string",
"POINT": "string",
"LIST": "string",
"MAP": "string",
}
def aura_type(neo4j_type: str) -> str:
return NEO4J_TYPE_TO_AURA.get(neo4j_type.upper(), "string")
def main():
uri = os.environ.get("NEO4J_URI")
user = os.environ.get("NEO4J_USERNAME", "neo4j")
password = os.environ.get("NEO4J_PASSWORD")
database = os.environ.get("NEO4J_DATABASE", "neo4j")
if not uri or not password:
sys.exit("ERROR: NEO4J_URI and NEO4J_PASSWORD must be set in .env or environment")
driver = neo4j.GraphDatabase.driver(uri, auth=(user, password))
try:
driver.verify_connectivity()
print(f"Connected: {uri}")
print("Fetching schema (sample=1000)...")
schema = get_structured_schema(driver, database=database, sample=1000)
# Annotate aura_data_type early — cardinality filter depends on it
for label, props in schema.get("node_props", {}).items():
for p in props:
p["aura_data_type"] = aura_type(p.get("type", ""))
for rel_type, props in schema.get("rel_props", {}).items():
for p in props:
p["aura_data_type"] = aura_type(p.get("type", ""))
# Fast data gate — exits before any expensive queries if DB is too empty
node_count = _get_node_count(driver, database)
schema.setdefault("metadata", {})["node_count"] = node_count
_validate_data(schema, node_count) # sys.exit(1) on failure; finally still runs
# Vector indexes
records, _, _ = driver.execute_query(
"SHOW INDEXES YIELD name, type, labelsOrTypes, properties, state, options "
"WHERE type = 'VECTOR'",
database_=database,
)
schema["metadata"]["vector_index"] = [dict(r) for r in records]
# Full-text indexes
records, _, _ = driver.execute_query(
"SHOW INDEXES YIELD name, type, labelsOrTypes, properties, state "
"WHERE type = 'FULLTEXT'",
database_=database,
)
schema["metadata"]["fulltext_index"] = [dict(r) for r in records]
# Cardinality — one query per STRING property on nodes and relationships
string_prop_count = sum(
1 for props in schema.get("node_props", {}).values()
for p in props if p.get("aura_data_type") == "string"
) + sum(
1 for props in schema.get("rel_props", {}).values()
for p in props if p.get("aura_data_type") == "string"
)
print(f"Checking cardinality for {string_prop_count} string properties...")
_enrich_with_cardinality(driver, database, schema)
finally:
driver.close()
# Cross-reference full-text index metadata with property entries
_mark_fulltext_indexed_props(schema)
with open(OUTPUT_PATH, "w") as f:
json.dump(schema, f, indent=2, default=str)
print(f"Schema saved → {OUTPUT_PATH}")
_print_gitignore_warning()
_print_summary(schema)
# ── Data gate ────────────────────────────────────────────────────────────────
def _get_node_count(driver, database: str) -> int:
records, _, _ = driver.execute_query(
"MATCH (n) RETURN count(n) AS node_count",
database_=database,
)
return records[0]["node_count"] if records else 0
def _validate_data(schema: dict, node_count: int) -> None:
rel_types = {r["type"] for r in schema.get("relationships", [])}
errors = []
if node_count < 2:
errors.append(f"Database contains {node_count} node(s) — at least 2 required.")
if not rel_types:
errors.append("Database contains no relationship types — at least 1 required.")
if errors:
print("\nERROR: AuraDB does not have enough data to create an agent:")
for e in errors:
print(f" • {e}")
print("\nLoad data into the database before running fetch_schema.py.")
print("schema.json was NOT written.")
sys.exit(1)
# ── Cardinality enrichment ────────────────────────────────────────────────────
def _enrich_with_cardinality(driver, database: str, schema: dict) -> None:
# APOC is assumed available — it is bundled with all AuraDB instances.
# apoc.meta.schema() returns property types and indexed flags in one call.
# Only STRING+indexed properties are filter targets that need cardinality queries.
records, _, _ = driver.execute_query(
"CALL apoc.meta.schema() YIELD value RETURN value",
database_=database,
)
apoc_schema = records[0]["value"] if records else {}
for label, props in schema.get("node_props", {}).items():
apoc_props = (apoc_schema.get(label) or {}).get("properties", {})
for prop in props:
if prop.get("aura_data_type") != "string":
continue
pname = prop["property"]
if apoc_props.get(pname, {}).get("indexed", False):
_check_cardinality(
driver, database, prop,
f"MATCH (n:`{label}`) WHERE n[$prop] IS NOT NULL "
"WITH DISTINCT n[$prop] AS v LIMIT $limit ",
pname,
)
else:
prop["low_cardinality"] = False
for rel_type, props in schema.get("rel_props", {}).items():
apoc_props = (apoc_schema.get(rel_type) or {}).get("properties", {})
for prop in props:
if prop.get("aura_data_type") != "string":
continue
pname = prop["property"]
if apoc_props.get(pname, {}).get("indexed", False):
_check_cardinality(
driver, database, prop,
f"MATCH ()-[r:`{rel_type}`]->() WHERE r[$prop] IS NOT NULL "
"WITH DISTINCT r[$prop] AS v LIMIT $limit ",
pname,
)
else:
prop["low_cardinality"] = False
def _check_cardinality(driver, database: str, prop: dict, base_query: str, pname: str) -> None:
try:
records, _, _ = driver.execute_query(
base_query + "RETURN count(v) AS cnt",
prop=pname, limit=CARDINALITY_THRESHOLD + 1, database_=database,
)
cnt = records[0]["cnt"] if records else 0
if cnt <= CARDINALITY_THRESHOLD:
prop["low_cardinality"] = True
records, _, _ = driver.execute_query(
base_query + "RETURN collect(v) AS values",
prop=pname, limit=CARDINALITY_THRESHOLD + 1, database_=database,
)
raw = records[0]["values"] if records else []
prop["values"] = sorted(str(v) for v in raw if v is not None)
else:
prop["low_cardinality"] = False
except Exception:
prop["low_cardinality"] = False
# ── Full-text index cross-reference ─────────────────────────────────────────
def _mark_fulltext_indexed_props(schema: dict) -> None:
ft_lookup: dict[str, set] = {}
for idx in schema.get("metadata", {}).get("fulltext_index", []):
for entity in idx.get("labelsOrTypes", []):
for p in idx.get("properties", []):
ft_lookup.setdefault(entity, set()).add(p)
for label, props in schema.get("node_props", {}).items():
for prop in props:
prop["has_fulltext_index"] = prop["property"] in ft_lookup.get(label, set())
for rel_type, props in schema.get("rel_props", {}).items():
for prop in props:
prop["has_fulltext_index"] = prop["property"] in ft_lookup.get(rel_type, set())
# ── Output ───────────────────────────────────────────────────────────────────
def _print_summary(schema: dict) -> None:
node_props = schema.get("node_props", {})
rel_props = schema.get("rel_props", {})
relationships = schema.get("relationships", [])
metadata = schema.get("metadata", {})
node_count = metadata.get("node_count", "?")
rel_type_count = len({r["type"] for r in relationships})
print(f"\n── Data Summary ────────────────────────────────────────────────────")
print(f" Nodes sampled (≥): {node_count} | Relationship types: {rel_type_count}")
print("\n── Node Labels & Properties ────────────────────────────────────────")
for label, props in node_props.items():
prop_strs = [f"{p['property']}({p['type']}→{p['aura_data_type']})" for p in props]
print(f" ({label}): {', '.join(prop_strs) if prop_strs else '(no properties)'}")
if rel_props:
print("\n── Relationship Properties ─────────────────────────────────────────")
for rel_type, props in rel_props.items():
prop_strs = [f"{p['property']}({p['type']}→{p['aura_data_type']})" for p in props]
print(f" [{rel_type}]: {', '.join(prop_strs)}")
if relationships:
print("\n── Relationship Patterns ───────────────────────────────────────────")
for r in relationships:
print(f" ({r['start']})-[:{r['type']}]->({r['end']})")
vector_indexes = metadata.get("vector_index", [])
if vector_indexes:
print("\n── Vector Indexes (usable in SimilaritySearch) ─────────────────────")
for idx in vector_indexes:
options = idx.get("options") or {}
config = options.get("indexConfig") or {}
dims = config.get("vector.dimensions", "?")
provider = options.get("indexProvider", "?")
print(
f" name={idx.get('name','?')} labels={idx.get('labelsOrTypes',[])} "
f"props={idx.get('properties',[])} state={idx.get('state','?')} "
f"dims={dims} provider={provider}"
)
else:
print("\n── Vector Indexes: none found ──────────────────────────────────────")
print(" SimilaritySearch requires a vector index — use neo4j-vector-index-skill.")
ft_indexes = metadata.get("fulltext_index", [])
if ft_indexes:
print("\n── Full-Text Indexes ────────────────────────────────────────────────")
for idx in ft_indexes:
print(
f" name={idx.get('name','?')} labels={idx.get('labelsOrTypes',[])} "
f"props={idx.get('properties',[])} state={idx.get('state','?')}"
)
else:
print("\n── Full-Text Indexes: none found ───────────────────────────────────")
low_card_rows = []
for label, props in node_props.items():
for p in props:
if p.get("low_cardinality"):
ft = " [fulltext]" if p.get("has_fulltext_index") else ""
vals = ", ".join(f'"{v}"' for v in p.get("values", []))
low_card_rows.append(f" ({label}).{p['property']}{ft}: {vals}")
for rel_type, props in rel_props.items():
for p in props:
if p.get("low_cardinality"):
ft = " [fulltext]" if p.get("has_fulltext_index") else ""
vals = ", ".join(f'"{v}"' for v in p.get("values", []))
low_card_rows.append(f" [{rel_type}].{p['property']}{ft}: {vals}")
if low_card_rows:
print(f"\n── Low Cardinality Properties (≤{CARDINALITY_THRESHOLD} values) ────────────────────────")
print(" ⚠ Include valid values in CypherTemplate parameter descriptions")
for row in low_card_rows:
print(row)
else:
print(f"\n── Low Cardinality Properties: none found (threshold: ≤{CARDINALITY_THRESHOLD}) ────")
print()
def _print_gitignore_warning() -> None:
gitignore = Path(__file__).parent.parent.parent / ".gitignore"
if gitignore.exists():
content = gitignore.read_text()
if "schema.json" not in content:
print("\nWARNING: schema.json not in .gitignore — add it to avoid committing graph metadata")
else:
print("\nNOTE: Add schema.json to .gitignore to avoid committing graph metadata")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Invoke an Aura Agent with a natural language query.
Usage:
python3 scripts/invoke_agent.py --org-id ORG --project-id PROJ --agent-id AGENT "your question"
python3 scripts/invoke_agent.py --agent-id AGENT "your question" --raw
"""
import argparse
import json
import os
import sys
from pathlib import Path
try:
from dotenv import load_dotenv
load_dotenv(Path(__file__).parent.parent / ".env")
except ImportError:
pass
try:
import requests
except ImportError:
sys.exit("requests not found — run: pip install requests")
BASE = "https://api.neo4j.io"
V2 = f"{BASE}/v2beta1"
def get_token() -> str:
client_id = os.environ.get("AURA_CLIENT_ID")
client_secret = os.environ.get("AURA_CLIENT_SECRET")
if not client_id or not client_secret:
sys.exit("ERROR: AURA_CLIENT_ID and AURA_CLIENT_SECRET must be set in .env or environment")
r = requests.post(
f"{BASE}/oauth/token",
auth=(client_id, client_secret),
data={"grant_type": "client_credentials"},
)
r.raise_for_status()
return r.json()["access_token"]
def main():
parser = argparse.ArgumentParser(description="Invoke an Aura Agent")
parser.add_argument("--org-id", default=os.environ.get("AURA_ORG_ID"))
parser.add_argument("--project-id", default=os.environ.get("AURA_PROJECT_ID"))
parser.add_argument("--agent-id", default=os.environ.get("AURA_AGENT_ID"))
parser.add_argument("query", help="Natural language query to send to the agent")
parser.add_argument("--raw", action="store_true", help="Print full JSON response (includes reasoning chain)")
args = parser.parse_args()
for field in ("org_id", "project_id", "agent_id"):
if not getattr(args, field):
flag = f"--{field.replace('_', '-')}"
env = f"AURA_{field.upper()}"
sys.exit(f"ERROR: {flag} required (or set {env} in .env)")
token = get_token()
url = f"{V2}/organizations/{args.org_id}/projects/{args.project_id}/agents/{args.agent_id}/invoke"
r = requests.post(
url,
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
json={"input": args.query},
)
r.raise_for_status()
response = r.json()
if args.raw:
print(json.dumps(response, indent=2))
return
data = response if isinstance(response, (list, str)) else response.get("data", response)
if data.get("type") == "error":
err = data.get("error", {})
sys.exit(f"Agent error ({err.get('status_code', '?')}): {err.get('message', 'unknown error')}")
for block in data.get("content", []):
if block.get("type") == "text":
print(block["text"])
usage = data.get("usage", {})
if usage:
total = usage.get("total_tokens", "?")
req = usage.get("request_tokens", "?")
resp = usage.get("response_tokens", "?")
print(f"\n[tokens — request: {req}, response: {resp}, total: {total}]")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Manage Aura Agents via the v2beta1 REST API.
Commands: list, create, get, update (PATCH), delete
"""
import argparse
import json
import os
import sys
from pathlib import Path
try:
from dotenv import load_dotenv
load_dotenv(Path(__file__).parent.parent / ".env")
except ImportError:
pass
try:
import requests
except ImportError:
sys.exit("requests not found — run: pip install requests")
BASE = "https://api.neo4j.io"
V2 = f"{BASE}/v2beta1"
def get_token() -> str:
client_id = os.environ.get("AURA_CLIENT_ID")
client_secret = os.environ.get("AURA_CLIENT_SECRET")
if not client_id or not client_secret:
sys.exit("ERROR: AURA_CLIENT_ID and AURA_CLIENT_SECRET must be set in .env or environment")
r = requests.post(
f"{BASE}/oauth/token",
auth=(client_id, client_secret),
data={"grant_type": "client_credentials"},
)
r.raise_for_status()
return r.json()["access_token"]
def headers(token: str) -> dict:
return {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
def agents_url(org_id: str, project_id: str, agent_id: str = "") -> str:
base = f"{V2}/organizations/{org_id}/projects/{project_id}/agents"
return f"{base}/{agent_id}" if agent_id else base
def unwrap(response: requests.Response):
data = response.json()
if isinstance(data, (list, str)):
return data
return data.get("data", data)
def cmd_list(args, token: str):
r = requests.get(agents_url(args.org_id, args.project_id), headers=headers(token))
r.raise_for_status()
agents = unwrap(r)
if not agents:
print("(no agents found)")
return
items = agents if isinstance(agents, list) else [agents]
for a in items:
status = "enabled" if a.get("enabled") else "disabled"
print(f"ID: {a['id']}")
print(f"Name: {a['name']}")
print(f"Status: {status}")
if a.get("endpoint_link"):
print(f"Endpoint: {a['endpoint_link']}")
if a.get("mcp_endpoint_link"):
print(f"MCP: {a['mcp_endpoint_link']}")
tools = a.get("tools", [])
if tools:
print(f"Tools: {', '.join(t.get('name', t.get('type', '?')) for t in tools)}")
print()
def cmd_create(args, token: str):
with open(args.config) as f:
payload = json.load(f)
r = requests.post(agents_url(args.org_id, args.project_id), headers=headers(token), json=payload)
r.raise_for_status()
data = unwrap(r)
print(json.dumps(data, indent=2))
agent_id = data.get("id")
if agent_id:
print(f"\nAgent created. Save this ID:\nAURA_AGENT_ID={agent_id}")
def cmd_get(args, token: str):
r = requests.get(agents_url(args.org_id, args.project_id, args.agent_id), headers=headers(token))
r.raise_for_status()
print(json.dumps(unwrap(r), indent=2))
def cmd_update(args, token: str):
with open(args.config) as f:
payload = json.load(f)
r = requests.patch(
agents_url(args.org_id, args.project_id, args.agent_id),
headers=headers(token),
json=payload,
)
r.raise_for_status()
print(json.dumps(unwrap(r), indent=2))
def cmd_delete(args, token: str):
r = requests.delete(agents_url(args.org_id, args.project_id, args.agent_id), headers=headers(token))
r.raise_for_status()
print(f"Agent {args.agent_id} deleted (status {r.status_code})")
def main():
parser = argparse.ArgumentParser(description="Manage Aura Agents (v2beta1)")
parser.add_argument("--org-id", default=os.environ.get("AURA_ORG_ID"), help="Organization ID")
parser.add_argument("--project-id", default=os.environ.get("AURA_PROJECT_ID"), help="Project ID")
sub = parser.add_subparsers(dest="command", required=True)
sub.add_parser("list", help="List all agents in project")
p_create = sub.add_parser("create", help="Create agent from JSON config file")
p_create.add_argument("--config", required=True, help="Path to JSON config (see assets/contract-review-agent.json)")
p_get = sub.add_parser("get", help="Get full agent details")
p_get.add_argument("--agent-id", default=os.environ.get("AURA_AGENT_ID"), required=False)
p_update = sub.add_parser("update", help="Partial update (PATCH) agent")
p_update.add_argument("--agent-id", default=os.environ.get("AURA_AGENT_ID"), required=False)
p_update.add_argument("--config", required=True, help="Path to JSON with fields to patch")
p_delete = sub.add_parser("delete", help="Delete agent (irreversible)")
p_delete.add_argument("--agent-id", default=os.environ.get("AURA_AGENT_ID"), required=False)
args = parser.parse_args()
if not args.org_id:
sys.exit("ERROR: --org-id required (or set AURA_ORG_ID in .env)")
if not args.project_id:
sys.exit("ERROR: --project-id required (or set AURA_PROJECT_ID in .env)")
for cmd in ("get", "update", "delete"):
if args.command == cmd and not args.agent_id:
sys.exit(f"ERROR: --agent-id required for {cmd} (or set AURA_AGENT_ID in .env)")
token = get_token()
dispatch = {
"list": cmd_list,
"create": cmd_create,
"get": cmd_get,
"update": cmd_update,
"delete": cmd_delete,
}
dispatch[args.command](args, token)
if __name__ == "__main__":
main()
Related skills
How it compares
Use as an agent-skill playbook with REST scripts, not a hosted MCP server or generic chat wrapper.
FAQ
Who is neo4j-aura-agent-skill for?
and small-team developers building GraphRAG or graph-aware assistants on Neo4j Aura who want API-driven agent lifecycle management from Claude Code, Cursor, or similar agents.
When should I use neo4j-aura-agent-skill?
During build when integrating agent-tooling against AuraDB—after loading a knowledge graph and before exposing an agent via REST or MCP to your product.
Is neo4j-aura-agent-skill safe to install?
The skill expects Aura client secrets and network calls to Neo4j cloud APIs; review the Security Audits panel on this page and scope credentials to a dedicated project before running manage scripts.