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.
# Scaffold a production project in the current directory without prompts
10xgraph init --template production --auth jwt --rate-limit redis --name MyAgent --yesWithout --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)
- agent.pyBuilds the ReactAgent and exports the compiled graph as
- 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.
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.
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.
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().
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.
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.
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.
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.
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.
{
"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
- Run the project locally: Run the server
- Replace the example tool: Use the @tool decorator
- Add your own fields to the state: Use custom state
- Look up every config key: Configuration reference
Related pages
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.