Skip to content

Project Structure

Understanding the layout of a tuvl project.

Directory Layout

A typical tuvl project has the following structure:

my-project/
├── pyproject.toml        # Project dependencies (uv-managed)
├── .env                  # Secrets and local config (git-ignored)
├── .env.example          # Safe-to-commit template
├── .gitignore
├── models/               # ModelDefinition YAMLs
│   ├── contact.yaml
│   └── order.yaml
├── workflows/            # Workflow YAMLs
│   ├── contact_intake.yaml
│   └── order_processing.yaml
├── datasources/          # DataSource connection YAMLs
│   └── postgres.yaml
├── llms/                 # AgentModel / LLM preset YAMLs
│   └── default.yaml
├── nodes/                # Python node implementations
│   ├── contact_nodes.py
│   └── order_nodes.py
├── .tuvl/                # tuvl project config (optional)
│   └── telemetry.yaml    # OTel export settings
└── tests/
    └── workflows/        # LLM-as-a-Judge test cases (*.yaml)

Generated by tuvl init --sample

Running tuvl init my-project --sample writes the full structure above, including a ready-to-run workflow, nodes, two test cases, and a telemetry config.

Core Directories

models/

YAML ModelDefinition files. Each one generates a SQLModel class, Pydantic schemas, and CRUD REST endpoints at /api/{model_name}.

models/contact.yaml
kind: "ModelDefinition"
version: "v1"
metadata:
  name: "Contact"
spec:
  tablename: "contacts"
  fields:
    - name: "id"
      type: "uuid"
      primary_key: true
      default: "uuid4"
      input: false
    - name: "email"
      type: "string"
      unique: true
      required: true
    - name: "name"
      type: "string"
      required: true

workflows/

YAML Workflow files. Each file's trigger section defines the HTTP endpoint tuvl mounts automatically.

workflows/contact_intake.yaml
kind: "Workflow"
version: "v1"
metadata:
  name: "contact_intake"
spec:
  context: "Contact"
  trigger:
    path: "/api/contacts"
    method: "POST"
  steps:
    - id: "save"
      kind: "functional"
      runner: "save_contact"
      routes:
        default: "prioritize"
    - id: "prioritize"
      kind: "agent"
      agent:
        model: "default"
        prompt: "Classify {{ name }} as high | medium | low priority. Return JSON: {\"priority\": \"...\"}"
        output:
          format: json
          map:
            priority: priority

datasources/

Database connection configurations. Supports multiple named datasources — reference them from model definitions with datasource: my_ds_name.

datasources/postgres.yaml
kind: "DataSource"
version: "v1"
metadata:
  name: "main"
spec:
  type: "postgresql"
  driver: "asyncpg"
  connection:
    host: "${POSTGRES_HOST}"
    port: ${POSTGRES_PORT:5432}
    database: "${POSTGRES_DB}"
    username: "${POSTGRES_USER}"
    password: "${POSTGRES_PASSWORD}"

llms/

AgentModel YAML files — reusable LLM provider presets. Reference them from workflow agent steps by name (e.g. model: "default").

llms/default.yaml
kind: "AgentModel"
version: "v1"
metadata:
  name: "default"
spec:
  provider: "ollama"
  model: "llama3"
  api_base: "http://localhost:11434"
  temperature: 0.7
  max_tokens: 1024

nodes/

Python files containing @node()-decorated async functions. tuvl auto-discovers and imports all .py files in this directory at startup — no __init__.py needed.

nodes/contact_nodes.py
from tuvl.core.nodes.base import node

@node("save_contact")
async def save_contact(ctx: dict) -> dict:
    repo = ctx["_db"]["Contact"]
    record = await repo.add({"email": ctx["email"], "name": ctx["name"]})
    ctx["id"] = str(record.id)
    return ctx

.tuvl/

Optional project-level tuvl config. Currently supports:

  • telemetry.yaml — OTel exporter settings (endpoint, service name, headers)

tests/workflows/

YAML test cases for tuvl test. Each file specifies stubs for external calls, an expected execution trace, and optional LLM-as-a-Judge evaluation assertions. See Testing Workflows.

Startup Sequence

When tuvl dev or tuvl run starts, the engine:

  1. Reads .env and environment variables
  2. Connects to all configured datasources
  3. Loads models/*.yaml → generates SQLModel classes + CRUD routes
  4. Loads nodes/*.py → registers @node() functions in NODE_REGISTRY
  5. Loads workflows/*.yaml → mounts HTTP trigger routes
  6. Loads llms/*.yaml → registers AgentModel presets
  7. Initialises OTel tracer if telemetry.yaml is present and enabled
  8. Starts the uvicorn server

Multi-Project Workspaces

For larger applications, run multiple tuvl projects side by side:

workspace/
├── services/
│   ├── crm/
│   │   ├── models/
│   │   ├── workflows/
│   │   └── nodes/
│   └── billing/
│       ├── models/
│       ├── workflows/
│       └── nodes/
└── docker-compose.yaml
# Run a specific service
tuvl dev --project-dir services/crm
tuvl dev --project-dir services/billing --port 8001

Next Steps

  • Architecture — How components interact at runtime
  • Models — Full model definition reference
  • Workflows — Workflow and step configuration