Skip to main content

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:

  • by accepts "user" as well as "ip" and "global". Switch to "user" once auth is enabled.
  • The limiter also gates WebSocket handshakes on /v1/graph/ws and /v1/graph/live, sharing the same bucket as REST. Exceeding it closes the handshake with code 1013.
  • "backend": "memory" counts per process and logs a startup warning. With N workers the real limit is requests 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:

  • levelspans (timing only), standard (default; token counts, model, params — no message content), or full (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 — leave null for the default, or set a regional base URL like https://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

FieldTypeDefaultDescription
pathstringnull (pytest auto-discovery)Directory or file to pass to pytest
coveragebooleanfalseEnable --cov and generate an HTML coverage report in htmlcov/
coverage_thresholdintegernullFail 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

FieldTypeDefaultDescription
directorystring"evals"Directory to scan for eval files (files matching *_eval.py or eval_*.py)
output_dirstring"eval_reports"Directory where HTML and JSON reports are written
thresholdfloatnullMinimum pass rate (0.0–1.0). The command exits with code 1 if the pass rate is below this value
parallelbooleanfalseRun eval cases concurrently instead of sequentially
max_concurrencyinteger4Maximum 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:app requires an app variable in graph/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 .env file and point agentflow.json to it: "env": ".env"