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
brewctlversion 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:writeandschemas:write; an OAuth coding agent needs manage authority only when its MCP tool replaces an existing bundle
What you will build
Section titled “What you will build”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.
Step 1: Create the bundle directory
Section titled “Step 1: Create the bundle directory”mkdir -p quickstart-catalog/{schemas,entities}cd quickstart-catalogCreate manifest.yaml:
bundle_name: quickstart-catalogversion: 1.0.0entity_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.yamlStep 2: Define the entity schemas
Section titled “Step 2: Define the entity schemas”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.
Step 3: Populate the entities
Section titled “Step 3: Populate the entities”Create entities/categories.yaml:
- code: footwear name: Footwear popularity: high- code: apparel name: Apparel popularity: high- code: home_goods name: Home Goods popularity: mediumCreate 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: budgetStep 4: Apply the bundle
Section titled “Step 4: Apply the bundle”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: OKGenerated tools: list_category, get_category, list_brand, get_brandIf 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):
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 }'Step 6: Chat with the agent
Section titled “Step 6: Chat with the agent”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:
- The response uses
north-aurora, the code stored in the bundle. - The tool call filters
categoryandtiertogether. SyntheticBrew Knowledge document search does not provide these exact typed field filters. - The
totalfield reports how many records matched, independently of the returned page size.
Step 7: Verify the schema
Section titled “Step 7: Verify the schema”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.
What you just built
Section titled “What you just built”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.
Step 8: Try five query patterns
Section titled “Step 8: Try five query patterns”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:descreturns[budget, mid, premium]— declaration head first (“desc” = top of the array).sort=tier:ascreturns[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 formbrewctl globs *.yaml flat, merges, validates uniqueness across files, and applies as one atomic bundle. See Bundles & layouts guide for the full discussion.
Next steps
Section titled “Next steps”- Read the Knowledge Graphs concept for the full picture
- See the Schema annotations reference for every
x-*annotation - Learn the hybrid pattern — combine Knowledge Graphs with an external MCP server for inventory
- Bring it to production: Admin guide and Enterprise deployment