Errors
This page lists the exceptions loomflow raises, when each one
happens, and what to do about it. Every framework exception
subclasses LoomError, so you can catch broadly
(except LoomError) or narrowly (except RateLimitError). A few
argument checks are the exception: they raise plain ValueError (see
ValueError).
Many problems are not raised at all: a budget cap, max_turns, a
denied tool call or a blocked guardrail ends the run cleanly and is
reported on the RunResult (see Errors vs.
interruptions).
LoomError
├── ConfigError (Agent / Memory / Model misconfiguration)
├── ModelError
│ ├── TransientModelError (retried automatically)
│ │ ├── RateLimitError (HTTP 429, Retry-After honored)
│ │ └── ... (408, 409, 5xx, network blips, timeouts)
│ └── PermanentModelError (NOT retried)
│ ├── AuthenticationError (HTTP 401 / 403)
│ ├── InvalidRequestError (HTTP 400: bad prompt or args)
│ └── ContentFilterError (provider safety filter)
├── OutputValidationError (output_schema validation failed)
├── RunTimeout (Agent(timeout=) exceeded)
├── RateLimitExceeded (your own rate limiter refused a step)
├── MCPError (MCP server failure)
├── MemoryStoreError (Memory backend failure)
├── FreshnessError (certified value too old)
├── LineageError (certified value from a disallowed source)
├── BudgetExceeded (for custom components; see below)
├── PermissionDenied (for custom components; see below)
├── ToolError (for custom components; see below)
├── SandboxError (for custom components; see below)
├── RuntimeJournalError (for custom components; see below)
└── CancelledByUser (for custom components; see below)What raises what
ConfigError
Misconfiguration, raised when the object is built. Examples:
Agent("...")withoutmodel=.Agent("...", model="bad-spec")for an unknown string.Agent("...", architecture="bogus")for an unknown architecture.Agent("...", run_until={...})with an empty condition or an unknown key.Agent.from_config(...)/Agent.from_dict(...)with an unknown key, at the top level or in[budget]; the message names the closest known key (max_turn→max_turns). Amodel=orprompt_caching=dict with an unknown key raisesConfigErrortoo.
Catch at startup; not retryable.
ModelError and subclasses
Raised by the model adapter. The framework’s RetryPolicy retries
TransientModelError automatically (3 attempts by default) and only
raises once retries are used up. PermanentModelError fails
immediately.
RateLimitError: the provider returned 429. The retry waits at least as long as the provider’sretry-after-msorRetry-Afterheader asks; a hint longer than the policy’smax_delay_sraises at once instead.- Other
TransientModelErrors: 408, 409, 5xx, timeouts, network errors, and anoverloaded_error/api_errorevent inside an Anthropic stream. AuthenticationError: your API key is wrong, missing or revoked, or lacks permission (401 / 403).InvalidRequestError: the request was malformed (oversize prompt, bad tool definition, unknown model name).ContentFilterError: the provider’s safety filter rejected the request or response.- A plain
PermanentModelErrorcovers the rest, including an OpenAIinsufficient_quota429 (out of credit, so waiting won’t help) and tools offered to GPT-6 Astra or GPT-6.1 Sol, which can’t call tools through Chat Completions.
The original SDK exception is attached as __cause__ (and .cause).
See RetryPolicy + error taxonomy.
OutputValidationError
A direct LoomError subclass (not a ModelError). Raised by
agent.run() when you passed output_schema= and the final answer
still doesn’t validate after output_validation_retries extra turns
(each retry shows the model its validation error). Carries the raw
text (raw), the target schema, and the Pydantic error (cause).
RunTimeout
The whole run took longer than Agent(timeout=) seconds. The run is
cancelled and no RunResult is returned. exc.seconds holds the
limit. A single slow model call is a TransientModelError instead.
See timeout.
RateLimitExceeded
Raised by TokenBucketRateLimiter(mode="raise") when a tenant’s
bucket is empty, before any provider call is made. Carries user_id
and retry_after (seconds). Not to be confused with RateLimitError,
which is the provider’s own 429. See Rate limiting.
MCPError
An MCP transport, handshake or protocol failure: the MCP SDK isn’t
installed (pip install 'loomflow[mcp]'), a session wasn’t
initialised, or a server asked for sampling with no handler
configured. The message names the server.
MemoryStoreError
The memory backend failed, for example a lazy Postgres / Redis connection that couldn’t open, or a failed RediSearch query. Often transient; check connectivity and retry.
FreshnessError / LineageError
Raised by require_freshness / require_lineage from
loomflow.data when a
CertifiedValue is past its freshness window or came from a source
the policy doesn’t allow. (check_freshness / check_lineage return
a bool instead of raising.)
ValueError
A few argument checks raise plain ValueError, not a LoomError:
- An
effort=that isn’t one ofminimal,low,medium,high,xhigh,max, raised byAgent(...)orrun(). EffortNotSupportedError, aValueErrorsubclass, whenstrict_effort=Trueand the model takes no reasoning effort at all. See Reasoning effort.output_validation_retriesbelow 0.- A
Workflowwhose edges, routes or defaults name a node that was never added, raised when the run starts, before any node runs. SkillError, aValueErrorsubclass, for aSKILL.mdthat breaks the Agent Skills spec (a name with a leading or trailing hyphen or--, XML tags in the description, …).
wait_for_signal(..., timeout_s=) raises the built-in TimeoutError
when no signal arrives in time.
Reserved for custom components
BudgetExceeded, PermissionDenied, ToolError, SandboxError,
RuntimeJournalError and CancelledByUser are part of the hierarchy
so your own budgets, tools, sandboxes and runtimes can raise typed
errors. The built-in components report these situations without
raising:
| Situation | How it shows up |
|---|---|
| Budget cap hit | result.interrupted = True, result.interruption_reason = "budget:<cap>" |
max_turns reached | result.interrupted = True, result.interruption_reason = "max_turns_exceeded" |
| Guardrail blocked the prompt or answer | result.interrupted = True, result.interruption_reason = "guardrail:<name>" |
| Permission denied / approval declined | A ToolResult with denied=True and a reason; the model sees it and adapts. An approval handler that raises, or returns anything but True, an action name or an ApprovalDecision, declines |
| A pre-tool hook denied, raised or timed out | The same denied ToolResult; for a hook that raised or timed out, reason names the hook |
| A tool raised | A ToolResult with ok=False and error=str(exc); the model sees it and adapts |
| Sandbox blocked a path | A denied ToolResult from FilesystemSandbox |
Catching broadly
from loomflow import Agent, LoomError
agent = Agent("...", model="claude-opus-4-7")
try:
result = await agent.run("...")
except LoomError as exc:
log.error("agent run failed: %s", exc)
return user_friendly_error()Catching narrowly
from loomflow import Agent, RunTimeout
from loomflow.core.errors import AuthenticationError, RateLimitError
agent = Agent("...", model="claude-opus-4-7", timeout=60)
try:
result = await agent.run("...")
except AuthenticationError:
raise HTTPException(401, "API key invalid")
except RateLimitError:
raise HTTPException(429, "rate limit; retry later")
except RunTimeout:
raise HTTPException(504, "agent took too long")
if result.interrupted and result.interruption_reason.startswith("budget:"):
raise HTTPException(402, f"budget exceeded: {result.interruption_reason}")agent.stream(...) raises the same exceptions, as-is, out of the
async for loop, and Workflow.parallel raises a failing branch’s own
exception. (Before 0.17 both could arrive wrapped in an
ExceptionGroup, so except* handlers written for that can go back
to plain except.)
classify_model_error(exc)
Turns a raw provider exception into the framework’s typed error.
Useful when you call a provider SDK yourself. It returns None when
the exception isn’t a recognised model error:
from loomflow.governance import classify_model_error
try:
response = await my_provider_call()
except Exception as exc:
typed = classify_model_error(exc)
if typed is None:
raise
raise typed from excErrors vs. interruptions
A RunResult with interrupted=True is a clean stop: a budget
cap, max_turns, a guardrail or a run_until limit. An exception
is a failure the framework couldn’t recover from. Most production
code handles both: check result.interrupted after a successful
return, and catch LoomError for the rest. A Workflow whose Agent
node comes back interrupted stops at that node, with
WorkflowResult.interrupted set and interruption_reason reading
"<node>:<reason>"; an interrupted branch of Workflow.parallel
stops it the same way ("fan_out:<branch>:<reason>", and merge
isn’t called).