Skip to Content
DocsProductionConfig file

Config file (0.9.37+)

You can build an Agent from a TOML file or a parsed dict instead of constructing it inline. The reason this exists isn’t ergonomics. It’s that production agents tend to have three different audiences who all want a say in the wiring:

  • Ops wants to flip backends per environment. SQLite locally, Postgres in prod, without rebuilding the image.
  • SRE wants budget caps and permission policies in a reviewed config file, not buried in Python.
  • Compliance wants the audit-log path declared declaratively so auditors can find it without reading code.

Agent.from_config(path) reads a TOML file. Agent.from_dict(cfg) takes the parsed dict directly. Both accept the same shape, so you can drive the same agent from a Pydantic BaseSettings, a Helm chart’s env vars, a YAML file plus yaml.safe_load, whatever you already have.

from loomflow import Agent agent = Agent.from_config("./agent.toml") result = await agent.run("hello", user_id="alice")

A complete agent.toml

This covers every block the resolver understands:

# ---------- top-level scalars --------------------------- instructions = "You are a research assistant." model = "claude-opus-4-7" architecture = "react" max_turns = 100 auto_consolidate = true auto_extract = true response_tone = "concise" effort = "medium" strict_effort = false # ---------- backend tables ------------------------------ [memory] backend = "sqlite" path = "./memory.db" [runtime] backend = "sqlite" path = "./journal.db" [telemetry] backend = "file" path = "./spans.jsonl" [audit_log] name = "./audit.jsonl" scope_full = true # opts into verbatim capture secret = "my-org-hmac-key" [permissions] backend = "standard" mode = "default" denied_tools = ["bash"] [budget] max_tokens = 200_000 max_cost_usd = 5.0 max_wall_clock_minutes = 10 soft_warning_at = 0.8 # ---------- arrays of tables ---------------------------- [[skills]] path = "./skills/research" label = "Bundled" [[mcp]] name = "git" transport = "stdio" command = "uvx" args = ["mcp-server-git", "--repo", "."] [[mcp]] name = "remote" transport = "http" url = "https://example.com/mcp" headers = { Authorization = "Bearer ..." }

Then:

agent = Agent.from_config("./agent.toml")

Every backend table goes through the same resolver Agent(...) uses for its kwargs. Anything you can build via Agent(memory="sqlite: ./mem.db", ...) you can declare here, and vice-versa.

The same config as a dict

from loomflow import Agent cfg = { "instructions": "You are a helpful assistant.", "model": "echo", "max_turns": 5, "memory": {"backend": "sqlite", "path": "./m.db"}, "telemetry": "memory", # string form "permissions": "strict", "budget": {"max_tokens": 10_000}, } agent = Agent.from_dict(cfg)

Reach for this when your config already lives somewhere structured. Pydantic BaseSettings, a service config endpoint, env-var loaders, a YAML file you’ve already parsed. The dict form skips the TOML parse and goes straight to the resolver.

Reference: every key

Top-level scalars

KeyTypeNotes
instructionsstrRequired. The system prompt.
modelstr or dictRequired. String spec ("claude-opus-4-7", "gpt-4o", "echo") or dict form {"name": ..., "effort": ..., "strict_effort": ...}.
architecturestr"react", "plan_and_execute", "reflexion", etc. See Architectures.
max_turnsintCap on agent-loop iterations.
auto_consolidateboolRun the consolidator after each turn.
auto_extractboolAuto-extract structured facts to memory.
response_tonestrPreset or free-form. See response_tone.
effortstrReasoning-effort dial. See Reasoning effort.
strict_effortboolHard-fail when the model can’t honour effort.

Backend tables

Each accepts the same string shorthand or dict form the matching Agent(...) kwarg accepts:

[memory]

[memory] backend = "sqlite" # "inmemory" / "sqlite" / "postgres" / "redis" / "chroma" path = "./memory.db" # backend-specific

Or use the string shorthand directly:

memory = "sqlite:./memory.db"

[runtime]

[runtime] backend = "sqlite" # "inproc" / "sqlite" / "postgres" path = "./journal.db"

[telemetry]

[telemetry] backend = "file" # "none" / "memory" / "console" / "file" / "otel" path = "./spans.jsonl" # required when backend = "file"

Or shorthand:

telemetry = "console" telemetry = "file:./spans.jsonl"

[audit_log]

[audit_log] name = "./audit.jsonl" # path; omit for in-memory scope_full = true # opt into verbatim capture (default: false) secret = "my-key" # optional, HMAC-signs every entry

Default is compliance-friendly: prompts truncated at 500 chars, outputs not recorded, tool results stripped to ok/denied/error/ reason. See Audit log attribution for the verbatim-capture story.

[permissions]

[permissions] backend = "standard" # "allow_all" / "strict" / "accept_edits" / "bypass" / "standard" mode = "default" # for standard backend allowed_tools = ["search", "fetch"] denied_tools = ["bash", "delete_file"]

Shorthand:

permissions = "strict"

[budget]

[budget] max_tokens = 200_000 max_input_tokens = 150_000 max_output_tokens = 50_000 max_cost_usd = 5.0 max_wall_clock_minutes = 10 soft_warning_at = 0.8 # log a warning at 80% of any cap

See Per-user budget caps.

Arrays of tables

[[skills]] — load one or more skill bundles:

[[skills]] path = "./skills/research" label = "Research" # optional; defaults to the directory name [[skills]] path = "./skills/concise"

[[mcp]] — connect to MCP servers. Each entry needs a name and transport ("stdio" or "http").

Stdio transport:

[[mcp]] name = "git" transport = "stdio" command = "uvx" args = ["mcp-server-git", "--repo", "."] env = { GIT_AUTHOR_NAME = "Loom" } # optional description = "Git operations" # optional

HTTP transport:

[[mcp]] name = "remote" transport = "http" url = "https://example.com/mcp" headers = { Authorization = "Bearer abc" }

The MCP entries get wrapped in an MCPRegistry and passed to tools=. If you also supply tools= as a kwarg, the kwarg wins and mcp = [...] raises a ConfigError. Pick one.

What you can’t put in TOML

TOML / JSON / YAML can’t express Python callables, custom hook objects, or stateful instances. These still come through kwargs:

from loomflow import Agent, tool @tool async def search(query: str) -> str: ... agent = Agent.from_config( "agent.toml", tools=[search], # real callable hooks=my_hook_registry, # custom HookRegistry instance retry_policy=my_retry_policy, # custom RetryPolicy approval_handler=handler, # custom approval handler secrets=my_secret_store, # custom Secrets implementation )

When a kwarg and a config entry both target the same backend, the kwarg wins. That’s the override path. Use a shared base TOML and layer per-environment Python overrides on top.

TOML requires Python 3.11+. Loom uses the stdlib tomllib module, which only exists from 3.11. On older Pythons, parse the file yourself (with tomli, tomlkit, whatever you have) and pass the result to Agent.from_dict(cfg).

Worked example

The framework ships examples/15_config_file.py writing a complete TOML, building an agent from it, and asserting every backend resolved to the expected concrete class. Runs offline against EchoModel. No API key needed.

python examples/15_config_file.py
Last updated on