Configure agentflow.json
agentflow.json is the single source of truth for how your API server behaves. This guide walks through each field with examples and explains when to use each option. For a complete reference, see agentflow.json Configuration Reference.
Basic structure
Every agentflow.json must have at least an agent field:
{
"agent": "graph.react:app"
}
This tells the server where to load your compiled graph from. All other fields are optional but recommended for production.
Core fields
agent (required)
The import path to your compiled graph, in the format module:attribute:
{
"agent": "graph.react:app"
}
This import happens when the server starts. The module graph.react is imported and the app attribute is retrieved. The app must be a CompiledGraph instance:
# graph/react.py
from agentflow.core.graph import StateGraph
state_graph = StateGraph(MyState)
# ... add nodes, edges ...
app = state_graph.compile() # app is the compiled graph
Why it matters: If this import fails, the server will not start. Common issues:
- The module does not exist (typo in the path)
- The attribute does not exist on the module
- The module has syntax errors
Debugging: Test the import manually:
python -c "from graph.react import app; print(type(app))"
env (optional)
Path to a .env file containing environment variables:
{
"agent": "graph.react:app",
"env": ".env"
}
The server uses python-dotenv to load this file before importing your graph module. This is useful for:
- Development secrets (API keys, database URLs) that should not be committed to git
- Local overrides of production variables for testing
Example .env:
# API keys
GOOGLE_API_KEY=sk-abc123...
OPENAI_API_KEY=sk-def456...
# Database connections
REDIS_URL=redis://localhost:6379/0
DATABASE_URL=postgresql://user:pass@localhost/mydb
# Feature flags
ENABLE_STREAMING=true
Your graph code accesses them:
import os
api_key = os.environ["GOOGLE_API_KEY"]
Important: Never commit .env files to version control. Add .env to .gitignore.
In production: Pass environment variables via container/process environment, not .env files:
export GOOGLE_API_KEY=...
export REDIS_URL=...
agentflow api --no-reload
Persistence: Checkpointer
Without a checkpointer
Each API request is stateless. There is no conversation history. The next request forgets all prior context.
With a checkpointer
Conversation state is saved after each step. By providing a thread_id, you can resume a conversation later:
# First request
curl -X POST http://localhost:8000/v1/graph/invoke \
-d '{
"messages": [{"role": "user", "content": "What is my name?"}],
"config": {"thread_id": "user-123"}
}'
# Second request (same thread, context is preserved)
curl -X POST http://localhost:8000/v1/graph/invoke \
-d '{
"messages": [{"role": "user", "content": "Remind me what I just asked."}],
"config": {"thread_id": "user-123"}
}'
Adding a checkpointer
Development (in-memory, lost on restart):
# graph/dependencies.py
from agentflow.storage.checkpointer import InMemoryCheckpointer
my_checkpointer = InMemoryCheckpointer()
{
"agent": "graph.react:app",
"checkpointer": "graph.dependencies:my_checkpointer"
}
Fast and easy, but state is lost when the server restarts.
Production (PostgreSQL + Redis):
Install the optional dependency:
pip install 10xscale-agentflow[pg_checkpoint]
Create the checkpointer:
# graph/dependencies.py
from agentflow.storage.checkpointer import PgCheckpointer
my_checkpointer = PgCheckpointer(
db_url="postgresql+asyncpg://user:password@localhost/agentflow",
redis_url="redis://localhost:6379/0",
table="state_checkpoints", # Optional: customize table name
)
{
"agent": "graph.react:app",
"checkpointer": "graph.dependencies:my_checkpointer"
}
This persists all state to a PostgreSQL database, enabling:
- Durable conversation history across server restarts
- Multi-server deployments (shared state)
- Data retention and compliance
Health check: Verify the checkpointer is accessible:
curl -X GET http://localhost:8000/v1/threads
If the database is unreachable, you get a 500 error.
Thread name generation
When a new conversation thread is created, generate a human-readable name instead of a raw UUID:
# graph/thread_name_generator.py
from agentflow_cli.src.app.utils.thread_name_generator import ThreadNameGenerator
class MyThreadNameGenerator(ThreadNameGenerator):
async def generate_name(self, messages: list[str]) -> str:
# Optionally use message context or an LLM
return "thoughtful-conversation"
my_generator = MyThreadNameGenerator()
{
"agent": "graph.react:app",
"thread_name_generator": "graph.thread_name_generator:MyThreadNameGenerator"
}
When a new thread is created via the API, it automatically gets a readable name like exploring-ideas instead of 550e8400-e29b-41d4-a716-446655440000.
Memory store
The store field points at a BaseStore instance. Without it the /v1/store/* endpoints report
that no store is configured, even if your graph uses one internally.
# graph/dependencies.py
from agentflow.storage.store import QdrantStore
from agentflow.storage.store.embedding import OpenAIEmbedding
my_store = QdrantStore(
embedding=OpenAIEmbedding(),
path="./qdrant_data",
)
For local development and tests, agentflow.qa.testing.InMemoryStore is a BaseStore
implementation with no external dependencies — it satisfies the same field:
from agentflow.qa.testing import InMemoryStore
my_store = InMemoryStore()
{
"agent": "graph.react:app",
"store": "graph.dependencies:my_store"
}
The value is a module:attribute path resolving to an instance, not a class. Loading fails at
startup with a clear error if the attribute is not a BaseStore.
Dependency injection
The injectq field points at an InjectQ container. Use it when your nodes or tools need
services resolved at startup: a database session factory, an HTTP client, a settings object.
# graph/dependencies.py
from injectq import InjectQ
container = InjectQ()
container.bind_instance(MyDatabase, MyDatabase(dsn=os.environ["DATABASE_URL"]))
{
"agent": "graph.react:app",
"injectq": "graph.dependencies:container"
}
The container is activated as the global instance when it loads, so the server's own bindings
(graph config, auth backend, media service, checkpointer) land in the same container as yours.
Bind a BaseRateLimitBackend here when using "backend": "custom" for rate limiting.
Redis
A single Redis URL for the server's own use, as a plain string:
{
"agent": "graph.react:app",
"redis": "redis://localhost:6379/0"
}
It backs the shared L2 tier of the thread-ownership cache used by the ownership and rbac
authorization backends. When it is unset the server falls back to the REDIS_URL environment
variable. With neither set, or without the redis package installed, the cache runs in-process
only and the server logs a warning; nothing breaks, but each worker pays its own first lookup per
thread.
This is separate from rate_limit.redis, which the rate limiter uses. Configure both if you want
both features backed by Redis:
{
"redis": "redis://localhost:6379/0",
"rate_limit": {
"backend": "redis",
"redis": { "url": "redis://localhost:6379/1" }
}
}
Rate limiting
Add a rate_limit block to turn on the sliding-window limiter. It is off until you do.
{
"agent": "graph.react:app",
"rate_limit": {
"enabled": true,
"backend": "memory",
"requests": 100,
"window": 60,
"by": "ip",
"trusted_proxy_headers": false,
"trusted_proxy_hops": 1,
"exclude_paths": ["/health", "/docs", "/redoc", "/openapi.json"],
"fail_open": true
}
}
Three things that are easy to miss:
byaccepts"user"as well as"ip"and"global". Switch to"user"once auth is enabled.- The limiter also gates WebSocket handshakes on
/v1/graph/wsand/v1/graph/live, sharing the same bucket as REST. Exceeding it closes the handshake with code1013. "backend": "memory"counts per process and logs a startup warning. With N workers the real limit isrequests x N.
Full reference: Rate Limiting. Step-by-step setup: Configure rate limiting.
WebSocket connection limits
Cap how many WebSocket connections a single server process will hold open:
{
"agent": "graph.react:app",
"websocket": {
"max_connections": 100
}
}
max_connections counts /v1/graph/ws and /v1/graph/live together. null, 0, or an absent
block means unlimited. A negative value raises a ValueError at startup.
When the cap is reached the handshake is refused before accept() with close code 1013, so the
client sees a clean rejection rather than a socket that opens and immediately dies. The slot is
released when the handler returns or the client disconnects.
The counter is per process, like the in-memory rate-limit backend. With four workers,
max_connections: 100 means up to 400 concurrent sockets across the deployment. Size it per
worker, not per cluster.
Observability
Send graph, node, LLM, and tool spans to Pydantic Logfire and/or LangSmith by adding an observability block. The server configures the exporters and attaches the tracing publisher at startup — you do not call any setup helper yourself.
{
"agent": "graph.react:app",
"observability": {
"level": "standard",
"logfire": { "enabled": true, "service_name": "my-agent", "send_to_logfire": true, "console": false },
"langsmith": { "enabled": true, "project": "my-agent", "endpoint": null }
}
}
Keep the secrets in .env, never in agentflow.json:
LOGFIRE_TOKEN=your-logfire-write-token
LANGSMITH_API_KEY=your-langsmith-api-key
Field notes:
level—spans(timing only),standard(default; token counts, model, params — no message content), orfull(adds prompt/completion content; PII risk, opt in deliberately).logfire.enabled/langsmith.enabled— turn each backend on independently; enable both to fan out to both.langsmith.endpoint— leavenullfor the default, or set a regional base URL likehttps://eu.api.smith.langchain.com/otel.- Requires the extra:
pip install '10xscale-agentflow[observability]'. If a backend is enabled but its package or key is missing, the server logs a warning and starts without that exporter.
For the equivalent Python API (setup_logfire / setup_langsmith / setup_observability and the dedicated publishers), see Send traces to Logfire and LangSmith.
Authentication
No authentication (development only)
{
"agent": "graph.react:app",
"auth": null
}
Or omit the auth field entirely. All requests are accepted.
JWT authentication
{
"agent": "graph.react:app",
"auth": "jwt"
}
Add required environment variables:
export JWT_SECRET_KEY="your-long-random-secret-key-at-least-32-chars"
export JWT_ALGORITHM="HS256"
agentflow api
Clients must now provide a valid JWT token in every request:
curl -H "Authorization: Bearer <token>" http://localhost:8000/v1/graph/invoke
Requests without a valid token get a 401 Unauthorized response.
Custom authentication
For integration with external identity providers (OAuth, SAML, custom API keys):
authenticate is synchronous and takes (request, response, credential) — the bearer token
arrives as credential. Do not declare it async def (the server calls it without await).
# graph/auth.py
from typing import Any
from fastapi import Request, Response
from fastapi.security import HTTPAuthorizationCredentials
from agentflow_cli import BaseAuth
class MyCustomAuth(BaseAuth):
def authenticate(
self, request: Request, response: Response,
credential: HTTPAuthorizationCredentials | None,
) -> dict[str, Any] | None:
# Custom header (API key) — or use `credential.credentials` for a bearer token.
api_key = request.headers.get("X-API-Key")
if not api_key:
return None
user = verify_api_key(api_key)
if not user:
return None
# Return user info (at least user_id) for authorization later.
return {"user_id": user.id, "role": user.role}
{
"agent": "graph.react:app",
"auth": {
"method": "custom",
"path": "graph.auth:MyCustomAuth"
}
}
Authorization (fine-grained permissions)
After authenticating, restrict which users can access which endpoints. Without an
authorization key the default is mode-based — "ownership" (owner-only threads) in
production, "allow_all" in development. For roles → permissions with no code, use the RBAC
config block:
{
"agent": "graph.react:app",
"auth": "jwt",
"authorization": {
"backend": "rbac",
"roles": { "admin": ["*"], "user": ["graph:invoke", "graph:stream", "checkpointer:read"] },
"default_scopes": [],
"isolation": "owner"
}
}
For arbitrary rules, subclass AuthorizationBackend. authorize takes
(user, resource, action, resource_id=None, **context):
# graph/auth.py
from typing import Any
from agentflow_cli.src.app.core.auth.authorization import AuthorizationBackend
class MyAuthorizationBackend(AuthorizationBackend):
async def authorize(
self, user: dict[str, Any], resource: str, action: str,
resource_id: str | None = None, **context: Any,
) -> bool:
role = user.get("role", "guest")
if resource == "graph" and action == "invoke":
return role == "admin"
if resource == "checkpointer" and action == "read":
return role in ("admin", "user")
return False
{
"agent": "graph.react:app",
"auth": "jwt",
"authorization": "graph.auth:MyAuthorizationBackend"
}
See the Authentication reference for the full scope catalog,
isolation_scope/scopes_for, and how the isolation policy reaches the storage layer.
Testing
The optional "test" section configures the agentflow test command, which is a thin wrapper around pytest.
Minimal example
{
"agent": "graph.react:app",
"test": {
"path": "tests/"
}
}
Full example with coverage
{
"agent": "graph.react:app",
"test": {
"path": "tests/",
"coverage": true,
"coverage_threshold": 80
}
}
Fields
| Field | Type | Default | Description |
|---|---|---|---|
path | string | null (pytest auto-discovery) | Directory or file to pass to pytest |
coverage | boolean | false | Enable --cov and generate an HTML coverage report in htmlcov/ |
coverage_threshold | integer | null | Fail the run if coverage falls below this percentage (--cov-fail-under) |
How fields interact with CLI flags
CLI flags always win. The agentflow.json values are fallbacks used when the flag is not provided:
# Uses path/coverage from agentflow.json
agentflow test
# Overrides path; coverage still comes from agentflow.json
agentflow test --path tests/unit/
# Overrides coverage; path still comes from agentflow.json
agentflow test --coverage
Evaluation
The optional "evaluation" section configures the agentflow eval command. It controls where eval files are discovered, where reports are written, and the default pass-rate threshold. It does not control evaluation criteria — those come from confeval.py inside your evals directory.
Minimal example
{
"agent": "graph.react:app",
"evaluation": {
"directory": "evals"
}
}
Full example
{
"agent": "graph.react:app",
"evaluation": {
"directory": "evals",
"output_dir": "eval_reports",
"threshold": 0.9,
"parallel": true,
"max_concurrency": 8
}
}
Fields
| Field | Type | Default | Description |
|---|---|---|---|
directory | string | "evals" | Directory to scan for eval files (files matching *_eval.py or eval_*.py) |
output_dir | string | "eval_reports" | Directory where HTML and JSON reports are written |
threshold | float | null | Minimum pass rate (0.0–1.0). The command exits with code 1 if the pass rate is below this value |
parallel | boolean | false | Run eval cases concurrently instead of sequentially |
max_concurrency | integer | 4 | Maximum number of cases running at the same time (only applies when parallel is true) |
How fields interact with CLI flags
CLI flags take priority over agentflow.json. JSON values are fallbacks:
# Uses all settings from agentflow.json
agentflow eval
# Overrides output_dir; other settings still come from agentflow.json
agentflow eval --output-dir ci_reports/
# Overrides threshold; directory still comes from agentflow.json
agentflow eval --threshold 0.95
# Overrides parallel and concurrency
agentflow eval --parallel --max-concurrency 16
Criteria configuration
Evaluation criteria (thresholds per criterion, judge model, match type) are not set in agentflow.json. They live in confeval.py inside your evals directory:
# evals/confeval.py
from agentflow.qa.evaluation import EvalConfig, CriteriaConfig, CriterionConfig
EVAL_CONFIG = EvalConfig(
criteria=CriteriaConfig(
tool_name_match=CriterionConfig.tool_name_match(threshold=1.0),
rouge_match=CriterionConfig.rouge_match(threshold=0.5),
node_order=CriterionConfig.node_order(threshold=0.8),
)
)
If no confeval.py is found, the built-in defaults are used.
Environment-specific configs
Create separate config files for dev, staging, and production:
config/
dev.json
staging.json
prod.json
config/dev.json (local development, no persistence):
{
"agent": "graph.react:app",
"env": ".env.dev",
"auth": null
}
config/staging.json (pre-production, with checkpointer):
{
"agent": "graph.react:app",
"env": ".env.staging",
"checkpointer": "graph.dependencies:pg_checkpointer",
"store": "graph.dependencies:qdrant_store",
"auth": "jwt"
}
config/prod.json (production, all features):
{
"agent": "graph.react:app",
"env": ".env.prod",
"checkpointer": "graph.dependencies:pg_checkpointer_prod",
"store": "graph.dependencies:qdrant_store_prod",
"thread_name_generator": "graph.thread_name_generator:ProductionNameGenerator",
"auth": "jwt",
"authorization": "graph.auth:ProductionAuthorizationBackend"
}
Start the server with the appropriate config:
# Development
agentflow api --config config/dev.json
# Production
MODE=production agentflow api --config config/prod.json --no-reload
Validating your config
Before deploying, verify the config is valid:
python -c "
import json
with open('agentflow.json') as f:
config = json.load(f)
print('Config is valid JSON')
print(f'Agent path: {config.get(\"agent\")}')
"
Then test that the graph can be imported:
python -c "from graph.react import app; print(f'Graph loaded: {type(app)}')
If both succeed, your config is likely valid. Start the server and test an endpoint:
agentflow api &
curl http://127.0.0.1:8000/ping
Common config issues
"Module not found" error
- Check the module path is spelled correctly
- Verify the module exists in your project
- Try importing manually:
python -c "from graph.react import app"
"Attribute not found" error
- Verify the attribute name exists on the module
- In the example,
graph.react:apprequires anappvariable ingraph/react.py
"Checkpointer database connection failed"
- Verify the database URL is correct
- Verify the database is running and accessible
- Test the connection:
psql postgresql://user:pass@localhost/agentflow
"JWT_SECRET_KEY not found"
- Set the environment variable:
export JWT_SECRET_KEY=... - Or add it to your
.envfile and pointagentflow.jsonto it:"env": ".env"