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 retrieve exact stored records with generated tools. End-to-end in about 15 minutes.

You will need:

  • SyntheticBrew Cloud access or a current SyntheticBrew Enterprise deployment.
  • A brewctl version compatible with your SyntheticBrew release
  • A configured chat model named primary-model (or substitute the name of your verified model below)
  • A scoped API token with agents:write and schemas:write; an OAuth coding agent needs manage authority only when its MCP tool replaces an existing bundle

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"
}
}
}

The x-ref: "category" annotation on brand.category declares the relationship and requires the category entity type to exist in the bundle. It does not check that every category value names an existing entity. Review those values in your source data or add that check to your data pipeline when referential integrity is required.

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 . \
--engine "$SYNTHETICBREW_URL" \
--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 (an unknown x-ref target type, schema validation error, or 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 "$SYNTHETICBREW_URL/api/v1/agents" \
-H "Authorization: Bearer $BREWCTL_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "quickstart-assistant",
"model": "primary-model",
"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\"})"
}'
# Bind the bundle through the capability API after the agent exists.
curl -X POST "$SYNTHETICBREW_URL/api/v1/agents/quickstart-assistant/capabilities" \
-H "Authorization: Bearer $BREWCTL_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type":"knowledge_graphs",
"config":{"bundles":["quickstart-catalog"]},
"enabled":true
}'
# Expose the agent through a chat-enabled schema.
curl -X POST "$SYNTHETICBREW_URL/api/v1/schemas" \
-H "Authorization: Bearer $BREWCTL_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name":"quickstart-catalog",
"description":"Knowledge Graph quickstart",
"entry_agent_id":"quickstart-assistant",
"chat_enabled":true
}'

Open the quickstart-catalog schema, expand the Test Flow panel at the bottom of Admin, 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: [
{id: "north-aurora", data: {code: "north-aurora", name: "North Aurora", category: "footwear", tier: "premium", headquarters: "Vancouver, Canada"}}
],
total: 1,
limit: 50,
offset: 0
}
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?

Verify three things:

  1. The response uses north-aurora, the code stored in the bundle.
  2. The tool call filters category and tier together. SyntheticBrew Knowledge document search does not provide these exact typed field filters.
  3. The total field reports how many records matched, independently of the returned page size.

Try a follow-up question:

What about budget-tier apparel brands?

Expected tool call:

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

The expected result is budget-basics. Check that the response cites that stored entity code and does not introduce a record that was absent from the tool result.

In 15 minutes you declared a domain ontology (2 schemas, 8 entities), applied it as an atomic bundle, bound it to an agent through the knowledge_graphs capability, and observed structured retrieval through generated tools. No agent code was required; SyntheticBrew generated the tools from the schemas.

The same list_brand tool and its related tools support a richer query surface. The examples below show calls an agent can make. Ask the corresponding question in Admin chat, then compare the Tool Call Log entry with the expected call.

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 . while inside the bundle directory), then call:

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

The response shape is {items: [{id, name, tier, category}], total} instead of bare IDs. The id key contains the value of the schema’s x-id-field (declared as code in this example), so a client can iterate items without knowing the original 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.)

Together, these calls support a “find candidates, then fetch the chosen records” workflow. Use the ID listing when full entity payloads would add unnecessary context, and measure the token result with your own catalog and model.

Step 9 (optional): Split entities across files

Section titled “Step 9 (optional): Split entities across files”

When entities/brands.yaml becomes difficult for your team to review or maintain, move that entity type 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.