Project structure

In shortThe files that 10xgraph init --template production generates, what each one does, and which 10xgraph.json keys init writes for you.

  • 11 min read
  • 9 sections
  • Updated
  • v0.10.0
  • Markdown

10xgraph init --template production scaffolds a deployable agent project: a graph/ package with a state class, a weather tool, a prompt-injection validator and lifecycle hooks, plus tests/, evals/, ruff and pre-commit setup, an env template and a generated 10xgraph.json. This page walks through every file and the config keys init writes.

Generate the project

Run init with the production template and the auth and rate-limit choices you want. The command below writes a JWT-protected project with Redis rate limiting and skips all prompts.

Terminal
# Scaffold a production project in the current directory without prompts
10xgraph init --template production --auth jwt --rate-limit redis --name MyAgent --yes

Without --yes or --non-interactive, init asks for each choice interactively. The template choice defaults to quick-start, and --yes without --template also gives you quick-start.

Option Values Default Purpose
--template quick-start, production quick-start Which project to scaffold. Quick-start is a minimal graph with no auth.
--auth none, jwt, custom none Authentication scheme. Needs --template production.
--rate-limit none, memory, redis none Rate-limit backend. Needs --template production.
--name text Directory name, or MyAgent when the path is . Agent name, used for the package name in pyproject.toml and APP_NAME in .env.example.
--path, -p directory . Where to scaffold the project.
--yes, -y flag off Accept defaults and do not prompt.
--non-interactive flag off Fail instead of prompting. Use it in CI.
--dry-run flag off List the files that would be created without writing anything.
--force, -f flag off Overwrite files that already exist.

Init never overwrites silently. If any target file exists, it stops with File already exists ... Use --force to overwrite. Run --dry-run first when you scaffold into a non-empty directory.

See what init creates

Init writes the tree below. The auth/ folder exists only with --auth custom; with none or jwt it is skipped.

  • 10xgraph.jsonConfig the API server and CLI read on every run
  • .env.exampleEnvironment template; copy to .env and add your API key
  • .python-versionPython version pin (3.13 in the template)
  • .pre-commit-config.yamlGit hooks for formatting, linting and security checks
  • pyproject.tomlDependencies, ruff, pytest and coverage settings
  • graph/Your agent package
    • agent.pyBuilds the ReactAgent and exports the compiled graph as app
    • state.pyCustom AgentState subclass
    • thread_name_generator.pyThreadNameGenerator subclass for naming threads
    • tools/Tool functions
      • weather_tool.pyExample tool with retries and progress events
    • validators/Input validation and lifecycle hooks
      • manager.pyBuilds the CallbackManager and registers everything
      • validators.pyPromptInjectionValidator configuration
      • lifecyle.pyLifecycle hooks (the filename spelling is the template’s)
  • auth/Only with --auth custom
    • agent_auth.pyBaseAuth subclass with an unimplemented authenticate method
  • evals/Evaluation files
    • weather_agents_eval.pyEval set built with EvalSetBuilder
    • user_simulator_eval.pySimulated-user scenarios
  • tests/Pytest suite
    • conftest.pySets a dummy GEMINI_API_KEY for tests
    • test_graph_nodes.pyChecks on the agent configuration and routing
    • test_catalog_tools.pyTests for get_weather
    • test_agent_eval.pyTests for the eval set and report writing

The quick-start template is smaller: graph/agent.py (a single-file agent with its tool), .env.example and 10xgraph.json.

How the pieces connect

The API server reads 10xgraph.json, imports graph.agent:app, and runs it. agent.py pulls in the state class, the tool and the callback manager, so each file below plugs into that one graph.

Piece Role
10xgraph.json Points the server at app, and at the auth, thread-name and DI hooks.
graph/state.py Defines the working state the graph reads and writes, saved by the checkpointer.
graph/tools/ Functions the model can call.
graph/validators/ Checks input before the graph runs and reacts to graph events.
tests/ Unit tests, run with 10xgraph test.
evals/ Behavior evaluations, run with 10xgraph eval.

Read the generated files

graph/agent.py: the entry point

This file builds a ReactAgent, compiles it with a checkpointer and the callback manager, and exports the result as app. It is the file "agent": "graph.agent:app" points at.

graph/agent.py
from datetime import datetime

from tenxgraph.core.state import MessageContextManager
from tenxgraph.prebuilt.agent import ReactAgent
from tenxgraph.storage.checkpointer import InMemoryCheckpointer
from dotenv import load_dotenv

from graph.state import WeatherState
from graph.tools.weather_tool import get_weather
from graph.validators.manager import callback_manager


load_dotenv()

checkpointer = InMemoryCheckpointer()

context_manager = MessageContextManager(
    max_messages=20,  # last 20 user messages will be kept in context
    remove_tool_msgs=True,
)

react_agent = ReactAgent(
    state=WeatherState(),
    model="google/gemini-2.5-flash",
    provider="google",
    system_prompt=[
        {
            "role": "system",
            "content": """
                You are a helpful assistant.
                Your task is to assist the user in finding information and answering questions.
                User Current Location: {user_location}
                If missing information, ask the user for clarification.
                Use tools when they help answer the user.
            """,
        },
        {
            "role": "user",
            "content": f"Today Date is {datetime.now().strftime('%Y-%m-%d')}",
        },
    ],
    trim_context=True,
    tools=[get_weather],
    context_manager=context_manager,
)

app = react_agent.compile(
    checkpointer=checkpointer,
    callback_manager=callback_manager,
)

{user_location} in the system prompt is filled from the user_location field of WeatherState. InMemoryCheckpointer loses all state on restart, so swap it for a durable checkpointer before you deploy (see the callout below).

graph/state.py: custom state

The state class extends AgentState with your own fields. Fields you add persist with each checkpoint and are available to prompts and tools.

graph/state.py
from tenxgraph.core import AgentState


class WeatherState(AgentState):
    user_location: str = ""

graph/tools/weather_tool.py: an example tool

The tool takes the model’s location argument plus three optional injected parameters. tool_call_id, state and emit are supplied by the runtime and are not shown to the model.

graph/tools/weather_tool.py
import logging
import random

from tenxgraph.core.state.stream_emitter import StreamEmitter

from graph.state import WeatherState


logger = logging.getLogger(__name__)

MAX_RETRIES = 3


def call_weather_api(location: str) -> str:
    is_failed = random.choice([True, False])  # noqa: S311
    if is_failed:
        raise Exception("Failed to fetch weather data due to a simulated API error.")
    return f"The weather in {location} is sunny"


def get_weather(
    location: str,
    tool_call_id: str | None = None,
    state: WeatherState | None = None,
    emit: StreamEmitter | None = None,
) -> str:
    """Get the current weather for a specific location."""
    if tool_call_id:
        logger.info(f"Tool call ID: {tool_call_id}")
    if state and hasattr(state, "context"):
        logger.info(f"Number of messages in context: {len(state.context)}")  # type: ignore
    if emit:
        emit.progress("Fetching weather data...")

    for i in range(MAX_RETRIES):
        try:
            return call_weather_api(location)
        except Exception as e:
            logger.error(f"Attempt {i + 1} failed: {e}")
            if emit:
                emit.progress(f"Attempt {i + 1} failed, retrying...")

    return f"Sorry, I couldn't fetch the weather for {location} after multiple attempts."

call_weather_api is a stub that fails at random, so the retry loop is easy to see. Replace it with a real API call. Add your own tools as new files in graph/tools/ and list them in tools=[...]. See Use the @tool decorator for how the schema is built.

graph/validators: input checks and lifecycle hooks

The validators/ package holds a prompt-injection guard, a lifecycle hook class, and a manager that registers both. manager.py creates the callback_manager that agent.py passes to compile().

graph/validators/validators.py
from tenxgraph.utils.validators import PromptInjectionValidator


prompt_validator = PromptInjectionValidator(
    strict_mode=True,
    max_length=1000,
    blocked_patterns=[],
    suspicious_keywords=[
        "ignore previous",
        "forget previous",
        "disregard previous",
        "bypass",
        "circumvent",
        "override",
        "disable",
        "remove restrictions",
        "token",
        "coupon",
        "free",
    ],
)

Tune suspicious_keywords to your domain. Words such as free or token block ordinary messages in some apps. See Protect against prompt injection.

graph/validators/manager.py
from tenxgraph.utils.callbacks import CallbackManager

from .lifecyle import AgentLifecycleHook
from .validators import prompt_validator


callback_manager = CallbackManager()
callback_manager.register_input_validator(prompt_validator)
callback_manager.register_lifecycle_hook(AgentLifecycleHook())

lifecyle.py defines AgentLifecycleHook(GraphLifecycleHook[WeatherState]) with stub implementations of seven async hooks, each returning None to keep state unchanged (on_graph_start returns the state it received).

Hook Called when
on_graph_start Before the graph runs. Use it to hydrate state, for example from the authenticated user.
on_graph_end After successful completion, before the final state sync.
on_graph_error An unhandled error escapes the execution loop. The exception is still re-raised.
on_interrupt Execution pauses at an interrupt point.
on_resume A paused run is about to resume.
on_checkpoint Before every durable checkpoint write.
on_state_update After each node’s result is merged into state.

Override only the hooks you need. The full contract is in Use callbacks.

graph/thread_name_generator.py: thread names

The generator names a thread from the user’s messages. The template returns a fixed placeholder string.

graph/thread_name_generator.py
from tenxgraph_api import ThreadNameGenerator


class MyNameGenerator(ThreadNameGenerator):
    async def generate_name(self, messages: list[str]) -> str:
        # TODO: Implement logic to generate thread name based on messages
        return "MyCustomThreadName"

Replace the return value with logic that derives a name from messages, for example a short summary of the first message.

auth/agent_auth.py: custom authentication

With --auth custom, init scaffolds a BaseAuth subclass that rejects every request with 401 until you implement authenticate. Its constructor logs a warning to remind you.

auth/agent_auth.py
import logging
from typing import Any

from tenxgraph_api import BaseAuth
from fastapi import HTTPException, Request, Response
from fastapi.security import HTTPAuthorizationCredentials


logger = logging.getLogger(__name__)


class AgentAuth(BaseAuth):
    """Verify the caller and return who they are."""

    def __init__(self) -> None:
        logger.warning(
            "AgentAuth.authenticate is not implemented yet: every request will get 401. "
            "Implement it in auth/agent_auth.py."
        )

    def authenticate(
        self,
        request: Request,
        response: Response,
        credential: HTTPAuthorizationCredentials,
    ) -> dict[str, Any] | None:
        if credential is None:
            raise HTTPException(status_code=401, detail="Missing credentials")

        # Replace this with a real check, for example verifying the bearer token with your
        # identity provider:
        #
        #     claims = verify_token(credential.credentials)  # raises on a bad token
        #     return {"user_id": claims["sub"], "roles": claims.get("roles", [])}
        raise HTTPException(status_code=401, detail="Authentication is not configured")

Return a dict with at least user_id. It becomes the thread and memory owner, so take it from the verified credential, never from a client-set header or body field. Optional roles and scopes feed the authorization backend. See Authentication.

evals and tests

evals/weather_agents_eval.py defines get_eval_config() (it returns EvalPresets.tool_usage(threshold=0.6)) and get_eval_set() (an EvalSetBuilder set with a greeting case and get_weather tool tests). evals/user_simulator_eval.py defines get_scenarios() and runs simulated-user conversations. Run them with 10xgraph eval.

The tests cover the agent configuration and routing (test_graph_nodes.py), the get_weather tool (test_catalog_tools.py) and the eval set and report writing (test_agent_eval.py). conftest.py only sets a dummy GEMINI_API_KEY, so the tests run offline. Run them with 10xgraph test. See Test and evaluate and Testing.

.env.example

The env template holds app, CORS, request-limit, security-header, Snowflake, Sentry and API key settings. Copy it to .env and fill in GOOGLE_API_KEY.

.env.example
APP_NAME="MyAgent"
APP_VERSION="0.1.0"
MODE="production"
LOG_LEVEL="INFO"
IS_DEBUG="false"
SUMMARY="MyAgent Backend"

ORIGINS="*"
ALLOWED_HOST="*"

ROOT_PATH="/"
DOCS_PATH=""
REDOCS_PATH=""

MAX_REQUEST_SIZE=10485760  # 10MB

SECURITY_HEADERS_ENABLED="true"
HSTS_ENABLED="true"

SNOWFLAKE_EPOCH=1609459200000
SNOWFLAKE_NODE_ID=1
SNOWFLAKE_WORKER_ID=2

SENTRY_DSN=""

# Only with --rate-limit redis
REDIS_URL=""

# Only with --auth jwt
JWT_SECRET_KEY=""
JWT_ALGORITHM="HS256"

GOOGLE_API_KEY=""

This excerpt omits the remaining HSTS and header settings. The Redis and JWT blocks appear only when you choose those options. Every variable is listed in the environment reference.

pyproject.toml, .python-version and pre-commit

pyproject.toml names the project after your agent, requires Python 3.12 or later, and depends on 10xgraph, 10xgraph-api and google-genai. A test extra adds pytest, pytest-asyncio, pytest-cov and pytest-env. It also configures ruff (line length 100, target py312) and pytest with coverage over graph.

.python-version pins 3.13. If you run 3.12, edit it. .pre-commit-config.yaml runs the standard pre-commit hooks, pyupgrade, ruff format, ruff and bandit on commit. Install them with pre-commit install.

If you use a different model provider, swap google-genai for that provider’s SDK and change model and provider in agent.py.

Understand the generated 10xgraph.json

Init builds 10xgraph.json from your answers and writes it after the template files, so the generated file contains only the keys below. For --auth custom --rate-limit redis, it looks like this.

10xgraph.json
{
  "agent": "graph.agent:app",
  "env": ".env",
  "auth": {
    "method": "custom",
    "path": "auth.agent_auth:AgentAuth"
  },
  "thread_name_generator": "graph.thread_name_generator:MyNameGenerator",
  "ag_ui": {
    "enabled": false
  },
  "authorization": "ownership",
  "injectq": "graph.agent:container",
  "rate_limit": {
    "enabled": true,
    "backend": "redis",
    "requests": 100,
    "window": 60,
    "by": "ip",
    "trusted_proxy_headers": false,
    "exclude_paths": ["/ping", "/docs", "/redoc", "/openapi.json"]
  }
}
Key Written when Value from init
agent Always graph.agent:app
env Always .env
auth Always null for none, "jwt" for JWT (needs JWT_SECRET_KEY and JWT_ALGORITHM), or {"method": "custom", "path": "auth.agent_auth:AgentAuth"}
ag_ui Always {"enabled": false}, the AG-UI endpoint is off
thread_name_generator Always null for quick-start, graph.thread_name_generator:MyNameGenerator for production
authorization Production with jwt or custom "ownership": a thread is accessible only to its owner. Other values are "allow_all" or a module:attribute backend.
injectq Production graph.agent:container (see the warning below)
rate_limit Production with a rate-limit backend enabled, backend, requests, window, by, trusted_proxy_headers, exclude_paths

Defaults come from the prompts or flags: 100 requests per 60 seconds, counted ip (interactive mode also offers global), with trusted_proxy_headers true only if you say you are behind a reverse proxy. Keys such as test, evaluation, remote_tools, checkpointer and store are not written for production, though you can add them yourself. The quick-start template adds none of these either. For every key and default, see the configuration reference, and for rate-limit options see Rate limiting.

Fix these before you deploy

What to do next

Frequently asked questions

Which template should I start from?
Use quick-start to try an idea, since it writes a graph, an env example and 10xgraph.json. Use production when the agent will be deployed, because it adds a state class, a tool, a prompt-injection validator, lifecycle hooks, evals, tests, ruff and pre-commit setup, and optional JWT or custom auth with rate limiting.
Does init overwrite my existing files?
No. If a file already exists, init stops with an error unless you pass --force. A 10xgraph.json that was already in the directory is protected by the same rule.
Can I add rate limiting without auth?
Not through init. The interactive prompts ask about rate limiting only after you choose jwt or custom auth. You can still add a rate_limit block to 10xgraph.json by hand.
Last updated for v0.10.0Edit this page on GitHubReport an issue