Skip to content

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:

trails new blog
cd blog

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 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:

trails server --watch

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:

trails new my-app --with-shape

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.