Skip to Content
Errors

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("...") without model=.
  • 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). A model= or prompt_caching= dict with an unknown key raises ConfigError too.

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’s retry-after-ms or Retry-After header asks; a hint longer than the policy’s max_delay_s raises at once instead.
  • Other TransientModelErrors: 408, 409, 5xx, timeouts, network errors, and an overloaded_error / api_error event 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 PermanentModelError covers the rest, including an OpenAI insufficient_quota 429 (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 of minimal, low, medium, high, xhigh, max, raised by Agent(...) or run().
  • EffortNotSupportedError, a ValueError subclass, when strict_effort=True and the model takes no reasoning effort at all. See Reasoning effort.
  • output_validation_retries below 0.
  • A Workflow whose edges, routes or defaults name a node that was never added, raised when the run starts, before any node runs.
  • SkillError, a ValueError subclass, for a SKILL.md that 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:

SituationHow it shows up
Budget cap hitresult.interrupted = True, result.interruption_reason = "budget:<cap>"
max_turns reachedresult.interrupted = True, result.interruption_reason = "max_turns_exceeded"
Guardrail blocked the prompt or answerresult.interrupted = True, result.interruption_reason = "guardrail:<name>"
Permission denied / approval declinedA 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 outThe same denied ToolResult; for a hook that raised or timed out, reason names the hook
A tool raisedA ToolResult with ok=False and error=str(exc); the model sees it and adapts
Sandbox blocked a pathA 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 exc

Errors 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).

Last updated on