Skip to content

Knowledge Graphs — Quickstart

This tutorial walks you through creating a working Knowledge Graph, binding it to an agent, and observing the agent use auto-generated MCP tools to answer questions deterministically. End-to-end in about 15 minutes.

You will need:

  • A running SyntheticBrew engine (CE, EE, or Cloud — same workflow). Engine 1.3.0 or later is required: it ships migration 011_capabilities_kg_constraint.yaml, which enables the knowledge_graphs capability type.
  • brewctl 0.3.0 or later — installation guide
  • An admin API token

A small Knowledge Graph called quickstart-catalog with two entity types — category and brand — and a single agent bound to it. By the end, the agent will answer the question “what premium-tier brands carry footwear?” by calling list_brand with a filter, returning structured results.

Terminal window
mkdir -p quickstart-catalog/{schemas,entities}
cd quickstart-catalog

Create manifest.yaml:

bundle_name: quickstart-catalog
version: 1.0.0
entity_types:
- name: category
schema_file: schemas/category.schema.json
entities_file: entities/categories.yaml
- name: brand
schema_file: schemas/brand.schema.json
entities_file: entities/brands.yaml

Create schemas/category.schema.json:

{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "category",
"title": "Category",
"description": "A product category in the quickstart catalog.",
"type": "object",
"x-id-field": "code",
"x-tool-expose": ["list", "get"],
"required": ["code", "name"],
"additionalProperties": false,
"properties": {
"code": {
"type": "string",
"pattern": "^[a-z][a-z0-9_-]{1,30}$",
"x-index": true
},
"name": {
"type": "string",
"minLength": 3,
"maxLength": 60
},
"popularity": {
"type": "string",
"enum": ["high", "medium", "low"],
"x-index": true
}
}
}

Create schemas/brand.schema.json:

{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "brand",
"title": "Brand",
"description": "A brand carried in the catalog under exactly one category.",
"type": "object",
"x-id-field": "code",
"x-tool-expose": ["list", "get"],
"required": ["code", "name", "category", "tier"],
"additionalProperties": false,
"properties": {
"code": {
"type": "string",
"x-index": true
},
"name": {
"type": "string"
},
"category": {
"type": "string",
"x-ref": "category",
"x-index": true
},
"tier": {
"type": "string",
"enum": ["budget", "mid", "premium"],
"x-index": true
},
"headquarters": {
"type": "string",
"x-content-type": "text"
}
}
}

Note the x-ref: "category" annotation on brand.category — the engine will validate at apply time that every category value matches an existing category entity’s code.

Create entities/categories.yaml:

- code: footwear
name: Footwear
popularity: high
- code: apparel
name: Apparel
popularity: high
- code: home_goods
name: Home Goods
popularity: medium

Create entities/brands.yaml:

- code: north-aurora
name: North Aurora
category: footwear
tier: premium
headquarters: Vancouver, Canada
- code: stride-co
name: Stride Co.
category: footwear
tier: mid
headquarters: Portland, USA
- code: harborline
name: Harborline Apparel
category: apparel
tier: mid
headquarters: Boston, USA
- code: oakwood-home
name: Oakwood Home
category: home_goods
tier: mid
headquarters: Stockholm, Sweden
- code: budget-basics
name: Budget Basics
category: apparel
tier: budget
Terminal window
brewctl kg apply . \
--endpoint http://localhost:18082 \
--token $BREWCTL_TOKEN

You should see output like:

Bundle 'quickstart-catalog' v1.0.0:
+ category (3 entities)
+ brand (5 entities)
Atomic apply: OK
Generated tools:
list_category, get_category,
list_brand, get_brand

If something is wrong (broken x-ref, schema validation error, tool name collision), the apply rejects with a structured error message — no partial state is persisted.

Step 5: Create an agent bound to the bundle

Section titled “Step 5: Create an agent bound to the bundle”

In the admin UI, create an agent (or via API):

Terminal window
curl -X POST http://localhost:18082/api/v1/agents \
-H "Authorization: Bearer $BREWCTL_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "quickstart-assistant",
"model_name": "glm-5",
"system_prompt": "You are bound to the quickstart-catalog knowledge graph.\nYou have read-only tools: list_category, get_category, list_brand, get_brand.\n\nMANDATORY workflow on every user question:\n 1. Identify which entity_type the question is about.\n 2. Use list_/get_ tools — NEVER invent entity codes or attribute values.\n 3. If a tool returns 0 results, say so explicitly. Suggest the closest existing entities by querying a related type.\n 4. Prefer popularity=high categories first when not specified.\n 5. Cite the entity code of every recommendation.\n\nFilter values must be ENTITY CODES (lowercase snake_case or kebab-case), not display names.\nFilters is an object, not a JSON-encoded string. Example:\n list_brand(filters={\"category\": \"footwear\", \"tier\": \"premium\"})",
"capabilities": [{
"type": "knowledge_graphs",
"config": {"bundles": ["quickstart-catalog"]}
}]
}'

Open the admin UI chat tab and send:

What premium-tier brands carry footwear?

Watch the agent’s reasoning trace:

Tool call: list_brand(filters={category: "footwear", tier: "premium"})
Tool result: {
items: [
{code: "north-aurora", name: "North Aurora", category: "footwear", tier: "premium", headquarters: "Vancouver, Canada"}
],
total: 1
}
Agent response:
There is 1 premium-tier brand carrying footwear:
• north-aurora — North Aurora (HQ: Vancouver, Canada)
Want me to list mid-tier footwear brands too?

Notice three things:

  1. The agent did not hallucinate codes. north-aurora is exactly what you put in the YAML.
  2. The agent filtered by both category and tier in a single tool call. Vector RAG cannot do this.
  3. The total field tells the agent there is exactly 1 result — no chance of missing one.

Try a follow-up question:

What about budget-tier apparel brands?

Expected tool call:

list_brand(filters={category: "apparel", tier: "budget"})

Result: exactly budget-basics. If the agent had only seen premium-tier brands at training time, it might have hallucinated; with the Knowledge Graph, the response is grounded in the live bundle.

In 15 minutes you declared a domain ontology (2 schemas, 8 entities), pushed it as an atomic bundle, bound it to an agent via the knowledge_graphs capability, and observed deterministic retrieval through auto-generated MCP tools. No agent code was written; the tools were generated automatically from the schemas.

Step 7 (engine 1.4.0+): try the five query patterns

Section titled “Step 7 (engine 1.4.0+): try the five query patterns”

If you are on engine 1.4.0 or later, the same list_brand tool plus its new siblings unlock a much richer query surface. Each example below is a single tool call your agent can make — paste any of them into the admin chat tab to see the response.

1. Batch get — fetch multiple entities in one round-trip. Response shape is {entities, not_found}; misses do not fail the call.

get_brand(ids=["north-aurora", "stride-co", "no-such-brand"])

2. Range filter on a numeric x-index field. (Requires a numeric or format: date-time property — add one to the brand schema if you want to try this on the quickstart bundle.)

list_brand(filters={founded_year: {gte: 2010, lte: 2020}})

3. Multi-value [in] filter on any x-index field.

list_brand(filters={tier: {in: ["premium", "luxury"]}})

4. Summary projection on list_brand_ids — cheap preview without full payloads. Requires the brand schema to declare x-summary-fields AND expose the list_ids tool. Update brand.schema.json:

"x-tool-expose": ["list", "get"],
"x-tool-expose": ["list", "get", "list_ids"],
"x-summary-fields": ["name", "tier", "category"],

Re-apply (brewctl kg apply ./quickstart-catalog), then call:

list_brand_ids(filters={category: "footwear"})

Response shape is {items: [{id, name, tier, category}], total} instead of bare ids. Note the id key — the engine normalises the x-id-field’s value (which the schema declares as code) into a generic id key in the projection payload, so the agent can iterate items without knowing your id field name.

5. Server-side sort with enum declaration order. Your brand schema declares tier: enum: [budget, mid, premium] (the quickstart’s order — low to high). With this declaration, sort directions read as:

  • sort=tier:desc returns [budget, mid, premium] — declaration head first (“desc” = top of the array).
  • sort=tier:asc returns [premium, mid, budget] — declaration tail first.

If you want desc to mean “premium-first” semantically (highest tier on top), flip the schema declaration to [premium, mid, budget]. Then desc returns [premium, mid, budget] — natural “best first” ordering.

list_brand_ids(
filters={category: "footwear"},
sort=[{field: "tier", order: "asc"}, {field: "code", order: "asc"}],
limit=5
)

(In the quickstart’s [budget, mid, premium] schema, tier:asc puts premium first — the high tier. If you flipped the declaration to [premium, mid, budget], the same result needs tier:desc.)

These five together cover the typical “find candidates → fetch the chosen few” pattern that compresses agent retrieval token cost by roughly 12× versus a single list_brand over the same matches.

Step 8 (engine 1.4.0+, optional): split your entities by category

Section titled “Step 8 (engine 1.4.0+, optional): split your entities by category”

For larger catalogs (≥100 entities per type) the canonical single-file entities/brands.yaml becomes hard to PR-review. Migrate to the split layout:

- name: brand
schema_file: schemas/brand.schema.json
entities_file: entities/brands.yaml
- name: brand
schema_file: schemas/brand.schema.json
entities_path: entities/brand/

Create the directory and split entities into multiple files by any axis (tier, category, region):

entities/brand/
├── premium.yaml ← array of premium-tier brands
├── mid.yaml ← array of mid-tier brands
└── budget-basics.yaml ← single entity, one-document form

brewctl globs *.yaml flat, merges, validates uniqueness across files, and applies as one atomic bundle. See Bundles & layouts guide for the full discussion.