Getting Started with Trails¶
Overview¶
This chapter takes you from zero to a running Trails application in about 15 minutes. You will install the framework, scaffold a project, write a capability, define a node type, and see data land in the knowledge graph. No RDF, SPARQL, or ontology knowledge required.
Learning Objectives¶
After this chapter you will be able to:
- Install Trails in a Python virtual environment
- Scaffold a new project with
trails new - Write a capability that returns data
- Define a node type with typed fields
- Run the development server and invoke capabilities
- Navigate the project directory structure
What is Trails?¶
Trails is Rails for knowledge-graph apps. Where Rails gives you models, controllers, routes, and migrations over a relational database, Trails gives you node types, capabilities, shapes, and policies over a knowledge graph.
The framework in one sentence: start with plain functions and data, grow into typed nodes, validation, reasoning, and policy -- without rewriting anything you already wrote.
Trails is built for apps where data has relationships, provenance matters, and multiple agents (human or AI) need to collaborate on a shared, auditable graph of knowledge. Think research assistants, compliance tools, healthcare intake systems, content pipelines.
If you just need a REST API over a relational database, use Django or FastAPI. Trails is for when your data is a graph, your users include LLM agents, and you want trust primitives (policy, provenance, identity) baked in from day one.
Installing Trails¶
Trails is pre-alpha and not yet on PyPI. Install it from the local source tree:
# Create and activate a virtual environment
python3 -m venv .venv
source .venv/bin/activate
# Install Trails in editable mode
pip install -e ./python
Verify the installation:
trails --version
# trails 0.1.0a0
trails doctor
# Runs health checks -- all green means you're ready
trails doctor checks your Python version, virtual environment, port
availability, project layout, FFI bindings, and more. Run it whenever
something feels off.
Your First Project¶
Scaffold a new project:
This creates two files:
blog/
app.py # your capabilities live here
trails.toml # project config (optional -- delete it and Trails infers from dirname)
That is the entire project. There is no shapes/ directory, no
ontology/ directory, no policies/ directory. Those are not missing
-- they are not needed yet. You will add them when you need them.
Templates¶
trails new accepts a --template flag for different starting points:
| Template | What you get |
|---|---|
minimal (default) |
One capability, one file |
agent |
Agent runtime with a planner and session |
kg |
Label-first KG operations with ctx.kg |
full |
Multi-file layout with shapes, policies, ontology |
For now, minimal is plenty.
Your First Capability¶
Open app.py. The scaffold has a hello capability. Add a second one
next to it:
from trails import capability
@capability
def hello(ctx):
return {"msg": "hi"}
@capability
def greet(ctx, name: str):
return {"msg": f"hello, {name}"}
That is it. Two functions, two capabilities. Each is automatically discoverable over MCP and HTTP, with provenance recorded on every call.
The @capability decorator accepts three forms:
# Bare -- id inferred from function name
@capability
def greet(ctx, name: str): ...
# Positional id
@capability("blog.greet")
def greet(ctx, name: str): ...
# Keyword id with metadata
@capability(id="blog.greet", description="Greet a user by name")
def greet(ctx, name: str): ...
All three produce the same result. Use whichever reads best.
The ctx parameter is injected by the runtime. It gives you access to
the knowledge graph (ctx.kg), LLM clients (ctx.llm), configuration
(ctx.config), and the ability to invoke other capabilities
(ctx.invoke). More on that in Core Concepts.
Your First Node Type¶
Capabilities that only return computed data are useful but limited. Most
apps need to store and retrieve structured data. That is what
@node_type is for.
from trails import capability, node_type
@node_type("Note", fields={"title": str, "body": str})
class Note: ...
@capability
def create_note(ctx, title: str, body: str) -> dict:
note = Note(title=title, body=body)
ctx.kg.add(note)
return {"id": note.id}
@capability
def list_notes(ctx) -> list:
notes = Note.where().fetch(ctx)
return [{"id": n.id, "title": n.title} for n in notes]
@node_type("Note", fields={"title": str, "body": str}) tells Trails:
writes labelled Note must carry a title (str) and a body (str).
Mismatches are rejected at write time with a readable error.
Note(title=..., body=...) validates and mints a UUIDv7 IRI.
ctx.kg.add(note) persists the instance. Note.where().fetch(ctx)
retrieves all notes as hydrated Python objects.
Notice: the original hello and greet capabilities are untouched.
They do not know Note exists. Adding a node type is purely additive
-- existing code keeps working without modification. This is the
progressive-enhancement promise.
Supported field types¶
| Type | Example |
|---|---|
str, int, float, bool |
{"title": str, "priority": int} |
datetime.datetime |
{"created_at": datetime.datetime} |
list[str], list[int], etc. |
{"tags": list[str]} |
Another @node_type class |
{"author": Author} (reference edge) |
Optional[T] / T \| None |
{"subtitle": Optional[str]} (nullable) |
Running the Server¶
Start the development server:
Trails auto-detects the transport:
- Piped to an MCP client (e.g., Claude Desktop) -- speaks MCP over stdio
- Run in a terminal -- starts an HTTP server on port 8000
For development with auto-reload on file changes:
Invoking capabilities¶
From another terminal, invoke a capability via the CLI:
# Direct invocation (useful for testing)
python -c "
import trails
result = trails.invoke('create_note', {'title': 'Hello', 'body': 'World'})
print(result)
"
Or use the interactive console:
trails console
# >>> trails.invoke('greet', {'name': 'Alice'})
# {'payload': {'msg': 'hello, Alice'}, 'capability': 'greet', 'provenance': '...', ...}
The trails console REPL comes preloaded with trails, Q, planners,
LLMClient, and a live ctx so you can explore interactively.
Directory Structure Conventions¶
As your project grows, Trails expects this layout:
my-app/
trails.toml # project config
app/
capabilities/ # @capability functions (auto-discovered)
notes.py
users.py
shapes/ # @shape classes (auto-discovered)
note_shape.py
policies/ # .cedar policy files
notes.cedar
ontology/ # .ttl ontology files (optional, for reasoning)
notes.ttl
tests/
test_notes.py
trails server auto-discovers files in app/capabilities/ and
app/shapes/. You do not register them manually.
For small projects, a single app.py file works just as well -- Trails
does not force the multi-file layout until you need it. Scaffold the
multi-file layout with:
Generators¶
Trails has Rails-style generators for common files:
# Generate a capability
trails g cap create_note title:str body:str
# Generate a shape
trails g sh Note title:str body:str
# Generate a resource (MCP)
trails g res notes
CLI Quick Reference¶
| Command | What it does |
|---|---|
trails new <name> |
Scaffold a new project |
trails server [--watch] |
Start the server (MCP or HTTP) |
trails console |
Interactive REPL |
trails doctor |
Health checks |
trails routes |
List registered capabilities |
trails g cap\|sh\|res <name> [fields...] |
Generate files |
trails kg query "<sparql>" |
Ad-hoc SPARQL query |
trails check |
Lint shapes, capabilities, policies |
What's Next¶
You have a running Trails app with capabilities and a node type. The next chapter explains why things work the way they do:
Core Concepts -- The knowledge graph mental model, capabilities in depth, the context object, shapes, and the progressive-enhancement philosophy that ties it all together.
For a guided, step-by-step walk from hello world through reasoning and policy, see the Growing Your KG App tutorial.