# Error Codes

> Every error code the SOAT API can return, with its HTTP status and what to do about it.

{/* Generated by scripts/generateErrorCodesPage.ts — do not edit by hand. */}

# Error Codes

Every failing request answers with the same body, whatever went wrong:

```json
{
  "error": {
    "code": "RESOURCE_NOT_FOUND",
    "message": "Project 'proj_abc123' not found.",
    "hint": "Check the id, and check that the credential can see the project that owns the resource…",
    "docs_url": "https://soat.ttoss.dev/docs/error-codes#resource_not_found",
    "meta": { "id": "proj_abc123" }
  }
}
```

- `code` is stable — branch on it, never on `message`.
- `message` describes this occurrence and may name ids or fields.
- `hint` says what to do about it, so a caller meeting a code for the first
  time can act without leaving the response.
- `docs_url` addresses the section for that code on this page.
- `meta` is present only for codes that carry structured context.

The same catalog is published as JSON at [/errors.json](/errors.json) and as the
`x-error-codes` extension of [/openapi.json](/openapi.json). Both are generated
from the server source, so a client can branch on codes without scraping this
page.

## Hints by status class

A code without a hint of its own inherits the one for its HTTP status, so every
code answers with something actionable:

| Status | Default hint |
| --- | --- |
| 400 | Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`. |
| 401 | Authenticate the request: send a valid bearer token or project API key in the `Authorization` header. |
| 403 | The credential is authenticated but lacks this action, or is scoped to another project. Grant the action in an IAM policy, or use a credential that has it. |
| 404 | Check the id and the project the credential can see, then retry. Listing the collection confirms whether the resource exists. |
| 409 | The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named in the message, then retry. |
| 410 | The resource existed and is gone for good. Create a new one; retrying against this id will keep failing. |
| 413 | The payload exceeds the accepted size. Send less in one request, or use the upload flow for large bodies. |
| 422 | The request was accepted and then could not be carried out. The failure is in the work, not the request shape — read `meta` and the trace for which step failed before retrying. |
| 429 | Back off and retry after the window named by `Retry-After`. |
| 500 | Retry with backoff. If it persists, the server log carries the detail the response deliberately omits. |
| 501 | Not implemented on this deployment. Retrying will not change the outcome. |
| 502 | An upstream dependency failed rather than this server itself. Retry with backoff and check that dependency. |
| 503 | A capability this operation needs is not configured on this deployment. Configure it — the message names which — rather than retrying. |

## All codes

| Code | Status | Meaning |
| --- | --- | --- |
| [`ACTIVITY_INVALID_CURSOR`](#activity_invalid_cursor) | 400 | The activity feed cursor is malformed or does not decode to a valid (created_at, id) position. |
| [`ACTOR_HAS_MESSAGES`](#actor_has_messages) | 409 | The actor has linked session messages and cannot be deleted. |
| [`ACTOR_NOT_FOUND`](#actor_not_found) | 400 | A referenced actor does not exist. |
| [`AGENT_AND_CHAT_EXCLUSIVE`](#agent_and_chat_exclusive) | 400 | An actor cannot have both an agent_id and a chat_id assigned simultaneously. Set one or the other, not both. |
| [`AGENT_HAS_DEPENDENTS`](#agent_has_dependents) | 409 | The agent is referenced by one or more generations or traces and cannot be deleted. Delete the dependent records first. |
| [`AGENT_NOT_FOUND`](#agent_not_found) | 400 | A referenced agent does not exist. |
| [`AI_PROVIDER_ERROR`](#ai_provider_error) | 502 | The upstream AI provider returned an error (e.g. insufficient credits, rate limit, or the provider is unreachable). |
| [`AI_PROVIDER_HAS_DEPENDENTS`](#ai_provider_has_dependents) | 409 | The AI provider still has dependents. Live references (chats, agents, model-route targets) always block deletion and must be deleted or repointed first. Soft dependents (price overrides, usage records) block only until force=true, which drops the overrides and unlinks usage history. The error meta reports the counts, a sample of offending IDs, and a `forcible` flag. |
| [`AI_PROVIDER_MISCONFIGURED`](#ai_provider_misconfigured) | 400 | The AI provider record is missing configuration the provider type requires — for example a `vertex` provider with no Google Cloud project in `config.project` and no service-account key file linked as its secret. |
| [`AI_PROVIDER_NOT_FOUND`](#ai_provider_not_found) | 400 | A referenced AI provider does not exist. |
| [`API_KEY_PROJECT_SCOPE`](#api_key_project_scope) | 403 | The credential (API key or OAuth token) is bound to one project and the request targets a different one. This project binding is a hard boundary that the owner’s admin role does not lift — admin only relaxes the project create/delete gate, never the per-credential project scope for resource operations. Mint a key scoped to the target project (or an unscoped key) to operate there. The error meta reports `scoped_project` and `requested_project`. |
| [`APPROVAL_ALREADY_RESOLVED`](#approval_already_resolved) | 409 | The approval item has already been resolved (approved, rejected, or expired) and cannot be resolved again. |
| [`APPROVAL_EXPIRED`](#approval_expired) | 409 | The approval item has expired and can never execute. Its supporting evidence is stale; a fresh proposal is required. |
| [`APPROVAL_INVALID_EDIT`](#approval_invalid_edit) | 400 | The edited arguments supplied on edit-then-approve must be a JSON object. |
| [`APPROVAL_NOT_FOUND`](#approval_not_found) | 404 | The approval item does not exist or is not accessible. |
| [`APPROVAL_REASON_REQUIRED`](#approval_reason_required) | 400 | A reason is required when rejecting an approval item. |
| [`BOOTSTRAP_ALREADY_COMPLETED`](#bootstrap_already_completed) | 409 | The instance already has at least one user, so the one-time bootstrap of the first admin cannot run again. |
| [`CHAIN_NOT_FOUND`](#chain_not_found) | 404 | The continuation chain does not exist or is not accessible. |
| [`CHAT_NOT_FOUND`](#chat_not_found) | 400 | A referenced chat does not exist. |
| [`CONVERTER_FAILED`](#converter_failed) | 422 | The ingestion converter (tool or agent) failed to run, an agent converter returned an async deferral (unsupported — agent converters are always awaited inline), or a tool converter returned an async deferral during synchronous ingestion (`?wait=true`, which cannot wait for a callback). |
| [`CONVERTER_OUTPUT_INVALID`](#converter_output_invalid) | 422 | The ingestion converter returned an unrecognized output shape. Expected a string, `{ pages: [{ text, page_number }] }`, or `{ status: "pending" }`. |
| [`EMBEDDING_NOT_CONFIGURED`](#embedding_not_configured) | 503 | The embedding service is not configured. Set EMBEDDING_PROVIDER and EMBEDDING_MODEL environment variables. |
| [`EXCEPTION_ALREADY_RESOLVED`](#exception_already_resolved) | 409 | The exception item has already been resolved and cannot be acknowledged or resolved again. |
| [`EXCEPTION_NOT_FOUND`](#exception_not_found) | 404 | The exception item does not exist or is not accessible. |
| [`FILE_ALREADY_INGESTED`](#file_already_ingested) | 409 | The file already backs a Document (a file can only be ingested once). Use POST /documents/\{document_id\}/ingest to re-process the existing document, or upload a new copy of the file to ingest it under a different path/strategy. |
| [`FILE_DOWNLOAD_URL_NOT_CONFIGURED`](#file_download_url_not_configured) | 500 | An ingestion rule with file_delivery: download_url requires SOAT_BASE_URL to be set — the URL is fetched by an external converter that cannot resolve a relative or localhost address. |
| [`FILE_HAS_DEPENDENTS`](#file_has_dependents) | 409 | The file is referenced by one or more records and cannot be deleted. Delete the dependent records first. |
| [`FILE_NOT_FOUND`](#file_not_found) | 400 | The referenced file does not exist. |
| [`FILE_PARSE_FAILED`](#file_parse_failed) | 400 | The referenced file contains no extractable text. |
| [`FILE_STORAGE_MISCONFIGURED`](#file_storage_misconfigured) | 500 | The file storage backend is not configured correctly — an unknown FILES_STORAGE_PROVIDER, an unsupported storage type, or a missing required variable (e.g. FILES_STORAGE_DIR for local, FILES_S3_BUCKET for s3). |
| [`FILE_TOO_LARGE_FOR_SYNC`](#file_too_large_for_sync) | 413 | The file is too large to ingest synchronously. Retry without `?wait=true` to run ingestion in the background and poll the document status. |
| [`FORBIDDEN`](#forbidden) | 403 | The authenticated user does not have permission to perform this action. |
| [`FORCED_TOOL_CHOICE_CANNOT_STOP`](#forced_tool_choice_cannot_stop) | 400 | The agent forces tool use on every step (`tool_choice: "required"`, or the `{ type: "tool" }` object form) but declares no `has_tool_call` stop condition, so no turn it runs could ever end by answering — only by exhausting `max_steps`. Declare the terminal condition, or drop the forcing to `"auto"` and force a specific step with `step_rules` instead. |
| [`FORMATION_DELETE_FAILED`](#formation_delete_failed) | 409 | One or more of the formation's resources could not be deleted, so the stack is left in `delete_failed`. `meta.failures` names each blocking resource (its logical id, type, and the underlying error) — the common case is a resource the platform refuses to delete on its own, such as an agent that has generation or trace history. Resolve those, then delete the formation again. |
| [`FORMATION_HANDLER_FAILED`](#formation_handler_failed) | 502 | A resource type registered by the deployment operator delegates its lifecycle to an external HTTP handler, and that handler did not answer successfully — it returned a non-2xx status, timed out, was unreachable, or replied with a body the protocol does not allow (a create with no `physical_resource_id`, for instance). `meta.resource_type` and `meta.request_type` name the type and the operation, and the handler's own message is relayed when it sent one. This is an operator-side failure: the template is not at fault and re-deploying it unchanged will fail the same way until the handler is fixed. |
| [`FORMATION_INVALID_METADATA`](#formation_invalid_metadata) | 400 | The formation-level `metadata` field contains a `sub`/`param`/`ref`/`ref_attr` substitution expression. That field is a static annotation bag and is never resolved; put deploy-time substitutions in the template top-level `metadata` block instead. |
| [`FORMATION_MISSING_PARAMETERS`](#formation_missing_parameters) | 400 | The formation template declares one or more required parameters (no default, not satisfied by use_previous_value) that were not supplied or were provided as an empty string. |
| [`FORMATION_REPLACE_CLEANUP_FAILED`](#formation_replace_cleanup_failed) | 500 | A deploy replaced a resource — the handler answered a new `physical_resource_id` — and the superseded resource could not be deleted, so it is still live while the formation's ledger already points at the replacement. The deploy itself succeeded: the desired state is realised, and this is carried on the formation (and on its operation) rather than thrown, so it never appears as a response status. `meta.failures` names each un-deleted resource as `{ logical_id, resource_type, physical_resource_id, error }`. Each one stays on the formation as pending cleanup and is retried on the next deploy and on teardown; resolve whatever refused the delete and deploy again. |
| [`GENERATION_ALREADY_IN_PROGRESS`](#generation_already_in_progress) | 409 | A generation is already in progress for this session. Wait for it to complete before starting a new one. |
| [`GENERATION_CONTENT_UNAVAILABLE`](#generation_content_unavailable) | 409 | The generation's content was never stored (the agent or project runs with trace_content_mode 'none') or has since been purged, so the turn cannot be replayed or curated. |
| [`GENERATION_FAILED`](#generation_failed) | 500 | An unexpected error occurred during agent generation. Check the trace for details. |
| [`GENERATION_NOT_COMPLETED`](#generation_not_completed) | 409 | The generation has not completed, so it has no finished turn to promote. Only a completed generation can be curated into a dataset item. |
| [`GENERATION_NOT_FOUND`](#generation_not_found) | 404 | The generation does not exist or is not in a pending state for tool output submission. |
| [`GUARDRAIL_HAS_REFERENCES`](#guardrail_has_references) | 409 | The guardrail is still attached to one or more tools, agents, or projects (`guardrail_ids`) and cannot be deleted. Detach every reference first (requires guardrails:DetachGuardrail). The error meta lists the referencing resources by scope. |
| [`GUARDRAIL_NOT_FOUND`](#guardrail_not_found) | 400 | A guardrail referenced by a `guardrail_ids` attachment does not exist in the project. |
| [`HOOK_INVALID_JSON`](#hook_invalid_json) | 400 | The inbound hook request body is not valid JSON. |
| [`HOOK_PAYLOAD_TOO_LARGE`](#hook_payload_too_large) | 413 | The inbound hook request body exceeds the 1 MiB limit. |
| [`INGESTION_CALLBACK_CONFLICT`](#ingestion_callback_conflict) | 409 | The document is no longer awaiting this conversion attempt — it already completed, timed out, or was superseded by a re-ingest. |
| [`INGESTION_CALLBACK_INVALID_TOKEN`](#ingestion_callback_invalid_token) | 401 | The ingestion-callback token is missing, malformed, expired, or does not match the target document. |
| [`INGESTION_RULE_GLOB_CONFLICT`](#ingestion_rule_glob_conflict) | 409 | An ingestion rule for this content_type_glob already exists in the project. |
| [`INGESTION_RULE_VALIDATION_FAILED`](#ingestion_rule_validation_failed) | 400 | The ingestion rule configuration is invalid (e.g. tool_id and agent_id are both set or both missing, the converter tool is a client tool, a soat/mcp converter tool is missing an action, content_type_glob is not a valid MIME type glob, or preset_parameters contains the reserved key "file" or "callback"). |
| [`INTERNAL_ERROR`](#internal_error) | 500 | An unhandled server-side failure. The message is always the constant "Internal Server Error" — the underlying exception is logged, never returned, so this error carries no detail a caller can act on beyond retrying. |
| [`INVALID_CRON_EXPRESSION`](#invalid_cron_expression) | 400 | The cron expression is invalid. Triggers require a strict 5-field cron expression evaluated in UTC. |
| [`INVALID_EVENT_PATTERN`](#invalid_event_pattern) | 400 | The event pattern is invalid. An event trigger subscribes with '*', 'prefix.*', or an exact event name, and a pattern in a platform namespace must match a registered event. |
| [`INVALID_OUTPUT_SCHEMA`](#invalid_output_schema) | 400 | The agent output_schema is invalid — it must be a JSON Schema object. |
| [`INVALID_TEMPLATE_TOKEN`](#invalid_template_token) | 400 | A \{\{...\}\} double-curly token was found that is not a \{\{secret:sec_...\}\} or \{\{context:\<key\>\}\} reference, or a \{\{context:\<key\>\}\} token was found outside execute.headers / mcp.headers. Double curly braces are reserved for those two reference kinds; use single braces (\{param\}) for URL path parameters. |
| [`INVALID_TOOL_CONTEXT_KEY`](#invalid_tool_context_key) | 400 | A tool_context key cannot be used as an HTTP header name. Keys are forwarded verbatim as \<prefix\>\<key\> headers, where the prefix is the deployment's TOOL_CONTEXT_HEADER_PREFIX (X-Soat-Context- by default), so a key may only contain letters, digits and the characters !#$%&'*+-.^_`\|~, and two keys must not map to the same header name (header names are case-insensitive). |
| [`MISSING_TOOL_CONTEXT_KEY`](#missing_tool_context_key) | 400 | A tool header references a \{\{context:\<key\>\}\} token whose key is not present in the tool_context for this call. The call is failed rather than sending the header with an empty value. |
| [`MODEL_LISTING_FAILED`](#model_listing_failed) | 502 | The provider rejected the model listing request, or answered it with something other than JSON. The provider's own status and message are carried in the error message. |
| [`MODEL_LISTING_UNSUPPORTED`](#model_listing_unsupported) | 400 | The AI provider type cannot enumerate the models it can run. Azure lists deployments an operator named rather than models, and Ollama lists whatever was pulled onto that host, so neither answers the question. |
| [`MODEL_ROUTE_HAS_DEPENDENTS`](#model_route_has_dependents) | 409 | The model route is referenced by one or more agents, or is a project's default_model_route_id, and cannot be deleted. Repoint or delete the referencing consumers first; the error meta reports the counts and a sample of agent IDs. |
| [`MODEL_ROUTE_NOT_FOUND`](#model_route_not_found) | 400 | A referenced model route does not exist in the project. |
| [`NAME_CONFLICT`](#name_conflict) | 409 | A resource with this name already exists in the project. Use a different name. |
| [`NOT_IMPLEMENTED`](#not_implemented) | 501 | The request is well-formed but names an option this API version does not implement. |
| [`NO_ACTIVE_RELEASE`](#no_active_release) | 409 | The agent has no active release to promote or abort. Set one with PUT /agents/\{agent_id\}/release first. |
| [`ORCHESTRATION_CYCLE_DETECTED`](#orchestration_cycle_detected) | 422 | The orchestration graph contains a cycle. Cycles are not supported. |
| [`ORCHESTRATION_DISPATCH_FAILED`](#orchestration_dispatch_failed) | 422 | A workflow on_enter dispatch's orchestration run settled in a non-success terminal status (failed, cancelled, or expired). |
| [`ORCHESTRATION_HUMAN_NODE_MISMATCH`](#orchestration_human_node_mismatch) | 400 | The specified node is not the active human node waiting for input. |
| [`ORCHESTRATION_MAX_ITERATIONS_EXCEEDED`](#orchestration_max_iterations_exceeded) | 422 | The orchestration run exceeded the maximum number of iterations. |
| [`ORCHESTRATION_NESTED_RUN_FAILED`](#orchestration_nested_run_failed) | 422 | A `loop` / `sub_orchestration` child run settled in a non-success terminal status (failed, cancelled, or expired) carrying no code of its own, so the node that started it failed too. A child that carries a code fails its parent under that code instead, so the cause reaches the run a caller reads (#1185). Matches ORCHESTRATION_DISPATCH_FAILED, the same rule for a workflow on_enter dispatch's run. |
| [`ORCHESTRATION_NODE_FAILED`](#orchestration_node_failed) | 422 | A node in the orchestration run failed to execute. |
| [`ORCHESTRATION_NOT_FOUND`](#orchestration_not_found) | 404 | The orchestration does not exist or is not accessible. |
| [`ORCHESTRATION_POLL_EXHAUSTED`](#orchestration_poll_exhausted) | 422 | A poll node reached its attempt limit without its exit condition becoming true (only raised when fail_on_timeout is set). |
| [`ORCHESTRATION_RUN_DEPTH_LIMIT`](#orchestration_run_depth_limit) | 409 | Starting the next `loop` / `sub_orchestration` child run would nest past the depth bound, so it was refused. This bounds a cycle no intra-graph validator can see — a graph whose `sub_orchestration` node names itself, directly or through a cycle of two graphs — which the orchestration cycle validator (intra-graph, and excluding loop nodes deliberately) cannot detect. The bound is the smaller of MAX_ORCHESTRATION_RUN_DEPTH (default 10) and the project's `max_run_depth`; the error meta names the depth reached, the limit, and which of the two set it (#1185). |
| [`ORCHESTRATION_RUN_NOT_AWAITING_INPUT`](#orchestration_run_not_awaiting_input) | 409 | The orchestration run is not awaiting input. |
| [`ORCHESTRATION_RUN_NOT_CANCELLABLE`](#orchestration_run_not_cancellable) | 409 | The orchestration run is already in a terminal state and cannot be cancelled. |
| [`ORCHESTRATION_RUN_NOT_FOUND`](#orchestration_run_not_found) | 404 | The orchestration run does not exist or is not accessible. |
| [`ORCHESTRATION_VALIDATION_FAILED`](#orchestration_validation_failed) | 400 | The orchestration graph is invalid (e.g. a dangling edge, a node missing a required field, a cycle, or an input_mapping that references a state key no upstream node writes). |
| [`OUTPUT_SCHEMA_STREAMING_UNSUPPORTED`](#output_schema_streaming_unsupported) | 400 | Streaming generation does not support output_schema. Set stream to false, or remove output_schema from the agent/request. |
| [`OUTPUT_SCHEMA_VALIDATION_FAILED`](#output_schema_validation_failed) | 502 | The model returned an object that does not satisfy the agent output_schema (or was not valid JSON at all). Upstream-caused like AI_PROVIDER_ERROR: the request was well-formed, the model output was not. |
| [`PDF_PARSE_FAILED`](#pdf_parse_failed) | 400 | The uploaded file could not be parsed as a PDF. Ensure the file is a valid PDF document. |
| [`PIPELINE_DEPTH_EXCEEDED`](#pipeline_depth_exceeded) | 422 | The pipeline tool exceeded the maximum nested execution depth (pipelines calling pipelines). |
| [`PIPELINE_INVALID_STEP`](#pipeline_invalid_step) | 400 | The pipeline tool configuration is invalid (e.g. no steps, a duplicate or malformed step id, a missing tool reference, a forward reference to a later step, or a step that targets a client tool). |
| [`PIPELINE_STEP_FAILED`](#pipeline_step_failed) | 422 | A step in the pipeline tool failed to execute. |
| [`POLICY_NOT_FOUND`](#policy_not_found) | 400 | A referenced policy does not exist. |
| [`PROJECT_DEFAULT_ROUTE_INHERITED`](#project_default_route_inherited) | 409 | A project's default_model_route_id cannot be cleared while consumers that bind neither an AI provider nor a model route inherit it — clearing it would leave them with no resolvable model. Repointing the default to another route is always allowed; the error meta reports the count and a sample of inheriting resource IDs. |
| [`PROJECT_HAS_DEPENDENTS`](#project_has_dependents) | 409 | The project has one or more dependent resources (agents, ai providers, tools, etc.) and cannot be deleted. Use force=true to delete dependents as well. |
| [`PROMOTION_GATE_UNMET`](#promotion_gate_unmet) | 409 | The release's promotion_gate has no passing eval run against the canary version. Run the eval with agent_version pinned to the canary, or abort the rollout. |
| [`QUEUE_DRIVER_MISCONFIGURED`](#queue_driver_misconfigured) | 500 | The orchestration queue driver is not configured correctly — an unknown ORCHESTRATION_QUEUE_DRIVER, or a missing required variable for the selected driver (e.g. ORCHESTRATION_QUEUE_SQS_QUEUE_URL for sqs). |
| [`QUOTA_CONFLICT`](#quota_conflict) | 409 | A quota with the same (project, scope, scope_ref, metric, window) already exists. The all-enforce precedence rule makes duplicates pure redundancy, so a duplicate is rejected instead of stored. |
| [`QUOTA_EXCEEDED`](#quota_exceeded) | 429 | An enforced quota has been exceeded for the request scope. The response carries a `Retry-After` header (seconds until the window resets) and a `meta` block naming the breached quota, metric, limit, window, and reset time. |
| [`QUOTA_UNENFORCEABLE`](#quota_unenforceable) | 409 | An enforced `cost_usd` quota with `on_unpriced: "block"` (the default) cannot be evaluated: the current window holds a pricing blackout — several metered events, none of them priced — so the aggregate is `0` however much was actually spent. The cap refuses the generation rather than waving through spend it cannot measure. No `Retry-After` is sent — the window resetting changes nothing; configure pricing for the models in use, or set the quota's `on_unpriced` to `"allow"` to accept unmeasurable spend explicitly. |
| [`REQUEST_REJECTED`](#request_rejected) | 400 | The HTTP framework rejected the request before it reached a handler — a payload over the size limit, an unsupported method, a malformed request line. The accompanying message carries the specific reason, and the response status is the one the framework chose rather than this default. |
| [`RESOURCE_NOT_FOUND`](#resource_not_found) | 404 | The requested resource does not exist or is not accessible. |
| [`SECRET_HAS_DEPENDENTS`](#secret_has_dependents) | 409 | The secret is referenced by one or more AI providers and cannot be deleted. Use force=true to delete dependents as well. |
| [`SECRET_NOT_DECRYPTABLE`](#secret_not_decryptable) | 500 | A stored trigger or webhook secret could not be decrypted — SECRETS_ENCRYPTION_KEY has changed since it was written. Rotating the secret replaces it; restoring the original key recovers every secret written under it. |
| [`SECRET_NOT_FOUND`](#secret_not_found) | 400 | A secret referenced by a \{\{secret:...\}\} token does not exist in this project. |
| [`SESSION_CLOSED`](#session_closed) | 409 | The session is closed and does not accept new messages or generation requests. Open a new session to continue. |
| [`SESSION_EXPIRED`](#session_expired) | 410 | The session has expired due to inactivity. Open a new session to continue. |
| [`SINGLE_SESSION_CONFLICT`](#single_session_conflict) | 409 | An open session already exists for this actor. Use the existing session or close it first. |
| [`SYSTEM_MESSAGE_NOT_ALLOWED`](#system_message_not_allowed) | 400 | A `role: "system"` entry was supplied in `messages`. System content never travels as a message on any SOAT surface — it goes in the `instructions` field instead — the same name on every surface (a completion request, a Chat, an Agent). Mirrors the AI SDK, whose `allowSystemInMessages` defaults to false because a system entry in a caller-supplied array is a prompt-injection vector. |
| [`TASK_AUTOMATION_CHAIN_LIMIT`](#task_automation_chain_limit) | 409 | The task has run too many machine-driven transitions back-to-back with no outside intervention, and the next one was refused. This bounds a cycle composed across modules — a workflow state dispatching work that transitions the task back into that same state — which neither the orchestration cycle validator (intra-graph) nor the workflow itself (where revisiting states is deliberate) can see. Any move by a person, a plain API key, or an approval resolution resets the chain. The ceiling is TASK_AUTOMATION_CHAIN_LIMIT (default 50) (#885). |
| [`TASK_AUTOMATION_PROVENANCE_MISSING`](#task_automation_provenance_missing) | 500 | An automation-caused transition was about to be persisted with no recorded cause: principal_id, generation_id, and orchestration_run_id would all be null. This is a writer bug, not a valid degraded record — the transition is rejected instead of being silently written (#792). |
| [`TASK_GUARD_REJECTED`](#task_guard_rejected) | 400 | The transition guard evaluated to false for this task. |
| [`TASK_NOT_FOUND`](#task_not_found) | 404 | The task does not exist or is not accessible. |
| [`TASK_PAYLOAD_INVALID`](#task_payload_invalid) | 400 | The task payload does not satisfy the workflow's payload_schema. |
| [`TASK_STATE_NOT_FOUND`](#task_state_not_found) | 400 | A task create request's `state` does not name a declared state of the workflow. |
| [`TASK_TRANSITION_CONFLICT`](#task_transition_conflict) | 409 | A concurrent change made the requested transition invalid from the current state, or the task is already closed. |
| [`TASK_TRANSITION_NOT_FOUND`](#task_transition_not_found) | 400 | The named transition does not exist in the workflow. |
| [`TEXT_ENCODED_TOOL_CALL`](#text_encoded_tool_call) | 502 | The model wrote a tool invocation as plain assistant text (e.g. a ```json block containing \{"name": "\", "arguments": \{…\}\}) instead of making a structured tool call, so the tool never ran. Upstream-caused like AI_PROVIDER_ERROR: returning that text as the answer would be a silent data-integrity failure, so the generation fails instead. |
| [`TOOL_AUTH_FAILED`](#tool_auth_failed) | 502 | An http-type tool's `execute.auth` credentials could not be turned into a request credential — malformed service account JSON, an unusable private key, or a token endpoint that rejected the assertion. The error `meta` carries the token endpoint's status and body when it responded. Distinct from `TOOL_HTTP_ERROR`, which is the tool's own target rejecting the call. |
| [`TOOL_CALL_NOT_SUPPORTED`](#tool_call_not_supported) | 422 | This tool type cannot be invoked server-side. Client tools must be executed by the calling client. |
| [`TOOL_DISPATCH_FAILED`](#tool_dispatch_failed) | 422 | A workflow on_enter `tool` dispatch did not return a result. The tool call was settled before it ran — blocked by a guardrail (class D or a class-B tripwire), or routed to human approval (class C), which a task dispatch cannot park on. The error `meta` carries the `tool_id` and the `outcome` that settled it. Dispatch an orchestration instead when the tool is approval-gated: its engine can park and resume. |
| [`TOOL_EGRESS_BLOCKED`](#tool_egress_blocked) | 403 | An http- or mcp-type tool's target is not publicly routable — a loopback, private, link-local (cloud metadata), CGNAT or IPv6 ULA address — and the deployment's TOOL_EGRESS_ALLOWED_HOSTS does not list it. Also returned when a hostname resolves to such an address, when a redirect leads to one, when the scheme is not http/https, or when the redirect chain is too long. The error `meta` carries `tool_url` and, when known, the offending `tool_address`. |
| [`TOOL_HTTP_ERROR`](#tool_http_error) | 502 | An http-type tool call returned a non-2xx response. The error `meta` carries the real upstream status code, response body, URL, and method. |
| [`TOOL_NOT_FOUND`](#tool_not_found) | 400 | A referenced tool does not exist. |
| [`TRIGGER_ACTION_NOT_ALLOWED`](#trigger_action_not_allowed) | 400 | A trigger field is incompatible with its type: `action` is only valid for tool targets (and required for soat/mcp tools), and `cron` is required for schedule triggers and rejected otherwise. |
| [`TRIGGER_CAUSATION_LIMIT`](#trigger_causation_limit) | 409 | An event trigger refused to extend the causal chain that reached it — it is already in the chain, or the chain has run too deep. |
| [`TRIGGER_CREATOR_UNAVAILABLE`](#trigger_creator_unavailable) | 409 | The trigger creator no longer exists, so the firing identity cannot be resolved. |
| [`TRIGGER_INPUT_INVALID`](#trigger_input_invalid) | 400 | The effective input is invalid for the target (empty agent input, or an orchestration input_schema violation). |
| [`TRIGGER_NOT_ACTIVE`](#trigger_not_active) | 409 | The trigger is inactive and cannot fire. |
| [`TRIGGER_RECURSION_FORBIDDEN`](#trigger_recursion_forbidden) | 403 | A trigger-scoped credential cannot fire a trigger, preventing unbounded trigger→trigger loops. |
| [`TRIGGER_TARGET_NOT_FOUND`](#trigger_target_not_found) | 400 | The trigger target does not exist in the project, or its type does not match target_type. |
| [`TRIGGER_TYPE_IMMUTABLE`](#trigger_type_immutable) | 400 | A trigger’s type cannot be changed after creation. |
| [`UNAUTHORIZED`](#unauthorized) | 401 | Authentication is required or the token is invalid. |
| [`UNSUPPORTED_FILE_TYPE`](#unsupported_file_type) | 400 | The referenced file has a content type that cannot be ingested. Supported types: application/pdf, text/plain, text/markdown. |
| [`UPLOAD_TOKEN_EXPIRED`](#upload_token_expired) | 410 | The upload token has expired. Request a new one. |
| [`UPLOAD_TOKEN_NOT_FOUND`](#upload_token_not_found) | 404 | The upload token does not exist. |
| [`UPLOAD_TOKEN_USED`](#upload_token_used) | 409 | The upload token has already been used. |
| [`VALIDATION_FAILED`](#validation_failed) | 400 | The request body failed validation. |
| [`WORKFLOW_HAS_OPEN_TASKS`](#workflow_has_open_tasks) | 409 | The workflow has one or more open tasks and cannot be deleted. |
| [`WORKFLOW_NOT_FOUND`](#workflow_not_found) | 404 | The workflow does not exist or is not accessible. |
| [`WORKFLOW_VALIDATION_FAILED`](#workflow_validation_failed) | 400 | The workflow definition is invalid (e.g. duplicate state names, not exactly one initial state, a transition referencing an unknown state, a malformed guard, or an on_enter dispatch missing its target). |

## Reference

### ACTIVITY_INVALID_CURSOR

**HTTP 400** — The activity feed cursor is malformed or does not decode to a valid (created_at, id) position.

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#activity_invalid_cursor`

### ACTOR_HAS_MESSAGES

**HTTP 409** — The actor has linked session messages and cannot be deleted.

**What to do:** The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named in the message, then retry.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#actor_has_messages`

### ACTOR_NOT_FOUND

**HTTP 400** — A referenced actor does not exist.

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#actor_not_found`

### AGENT_AND_CHAT_EXCLUSIVE

**HTTP 400** — An actor cannot have both an agent_id and a chat_id assigned simultaneously. Set one or the other, not both.

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#agent_and_chat_exclusive`

### AGENT_HAS_DEPENDENTS

**HTTP 409** — The agent is referenced by one or more generations or traces and cannot be deleted. Delete the dependent records first.

**What to do:** The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named in the message, then retry.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#agent_has_dependents`

### AGENT_NOT_FOUND

**HTTP 400** — A referenced agent does not exist.

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#agent_not_found`

### AI_PROVIDER_ERROR

**HTTP 502** — The upstream AI provider returned an error (e.g. insufficient credits, rate limit, or the provider is unreachable).

**What to do:** The upstream model provider failed, not this server. Check the provider credential and the account behind it (credits, rate limits, model access), then retry with backoff.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#ai_provider_error`

### AI_PROVIDER_HAS_DEPENDENTS

**HTTP 409** — The AI provider still has dependents. Live references (chats, agents, model-route targets) always block deletion and must be deleted or repointed first. Soft dependents (price overrides, usage records) block only until force=true, which drops the overrides and unlinks usage history. The error meta reports the counts, a sample of offending IDs, and a `forcible` flag.

**What to do:** The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named in the message, then retry.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#ai_provider_has_dependents`

### AI_PROVIDER_MISCONFIGURED

**HTTP 400** — The AI provider record is missing configuration the provider type requires — for example a `vertex` provider with no Google Cloud project in `config.project` and no service-account key file linked as its secret.

**What to do:** The provider record is missing something it needs to make a call — usually the API key secret or the base URL. Update the AI provider and retry.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#ai_provider_misconfigured`

### AI_PROVIDER_NOT_FOUND

**HTTP 400** — A referenced AI provider does not exist.

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#ai_provider_not_found`

### API_KEY_PROJECT_SCOPE

**HTTP 403** — The credential (API key or OAuth token) is bound to one project and the request targets a different one. This project binding is a hard boundary that the owner’s admin role does not lift — admin only relaxes the project create/delete gate, never the per-credential project scope for resource operations. Mint a key scoped to the target project (or an unscoped key) to operate there. The error meta reports `scoped_project` and `requested_project`.

**What to do:** This API key is scoped to a single project and the request targets another one (or none). Omit `project_id` to use the key's own project, or use an unscoped credential.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#api_key_project_scope`

### APPROVAL_ALREADY_RESOLVED

**HTTP 409** — The approval item has already been resolved (approved, rejected, or expired) and cannot be resolved again.

**What to do:** The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named in the message, then retry.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#approval_already_resolved`

### APPROVAL_EXPIRED

**HTTP 409** — The approval item has expired and can never execute. Its supporting evidence is stale; a fresh proposal is required.

**What to do:** The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named in the message, then retry.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#approval_expired`

### APPROVAL_INVALID_EDIT

**HTTP 400** — The edited arguments supplied on edit-then-approve must be a JSON object.

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#approval_invalid_edit`

### APPROVAL_NOT_FOUND

**HTTP 404** — The approval item does not exist or is not accessible.

**What to do:** Check the id and the project the credential can see, then retry. Listing the collection confirms whether the resource exists.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#approval_not_found`

### APPROVAL_REASON_REQUIRED

**HTTP 400** — A reason is required when rejecting an approval item.

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#approval_reason_required`

### BOOTSTRAP_ALREADY_COMPLETED

**HTTP 409** — The instance already has at least one user, so the one-time bootstrap of the first admin cannot run again.

**What to do:** The first admin already exists, so bootstrap is closed for good. Sign in instead, and create further users with `POST /api/v1/users`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#bootstrap_already_completed`

### CHAIN_NOT_FOUND

**HTTP 404** — The continuation chain does not exist or is not accessible.

**What to do:** Check the id and the project the credential can see, then retry. Listing the collection confirms whether the resource exists.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#chain_not_found`

### CHAT_NOT_FOUND

**HTTP 400** — A referenced chat does not exist.

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#chat_not_found`

### CONVERTER_FAILED

**HTTP 422** — The ingestion converter (tool or agent) failed to run, an agent converter returned an async deferral (unsupported — agent converters are always awaited inline), or a tool converter returned an async deferral during synchronous ingestion (`?wait=true`, which cannot wait for a callback).

**What to do:** The request was accepted and then could not be carried out. The failure is in the work, not the request shape — read `meta` and the trace for which step failed before retrying.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#converter_failed`

### CONVERTER_OUTPUT_INVALID

**HTTP 422** — The ingestion converter returned an unrecognized output shape. Expected a string, `{ pages: [{ text, page_number }] }`, or `{ status: "pending" }`.

**What to do:** The request was accepted and then could not be carried out. The failure is in the work, not the request shape — read `meta` and the trace for which step failed before retrying.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#converter_output_invalid`

### EMBEDDING_NOT_CONFIGURED

**HTTP 503** — The embedding service is not configured. Set EMBEDDING_PROVIDER and EMBEDDING_MODEL environment variables.

**What to do:** Knowledge search needs an embedding model. Configure one on the project or the AI provider before ingesting or searching.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#embedding_not_configured`

### EXCEPTION_ALREADY_RESOLVED

**HTTP 409** — The exception item has already been resolved and cannot be acknowledged or resolved again.

**What to do:** The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named in the message, then retry.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#exception_already_resolved`

### EXCEPTION_NOT_FOUND

**HTTP 404** — The exception item does not exist or is not accessible.

**What to do:** Check the id and the project the credential can see, then retry. Listing the collection confirms whether the resource exists.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#exception_not_found`

### FILE_ALREADY_INGESTED

**HTTP 409** — The file already backs a Document (a file can only be ingested once). Use POST /documents/\{document_id\}/ingest to re-process the existing document, or upload a new copy of the file to ingest it under a different path/strategy.

**What to do:** The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named in the message, then retry.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#file_already_ingested`

### FILE_DOWNLOAD_URL_NOT_CONFIGURED

**HTTP 500** — An ingestion rule with file_delivery: download_url requires SOAT_BASE_URL to be set — the URL is fetched by an external converter that cannot resolve a relative or localhost address.

**What to do:** Retry with backoff. If it persists, the server log carries the detail the response deliberately omits.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#file_download_url_not_configured`

### FILE_HAS_DEPENDENTS

**HTTP 409** — The file is referenced by one or more records and cannot be deleted. Delete the dependent records first.

**What to do:** The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named in the message, then retry.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#file_has_dependents`

### FILE_NOT_FOUND

**HTTP 400** — The referenced file does not exist.

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#file_not_found`

### FILE_PARSE_FAILED

**HTTP 400** — The referenced file contains no extractable text.

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#file_parse_failed`

### FILE_STORAGE_MISCONFIGURED

**HTTP 500** — The file storage backend is not configured correctly — an unknown FILES_STORAGE_PROVIDER, an unsupported storage type, or a missing required variable (e.g. FILES_STORAGE_DIR for local, FILES_S3_BUCKET for s3).

**What to do:** Retry with backoff. If it persists, the server log carries the detail the response deliberately omits.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#file_storage_misconfigured`

### FILE_TOO_LARGE_FOR_SYNC

**HTTP 413** — The file is too large to ingest synchronously. Retry without `?wait=true` to run ingestion in the background and poll the document status.

**What to do:** The file is too large to parse inside the request. Call the same operation without `wait=true` and poll the handle it returns.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#file_too_large_for_sync`

### FORBIDDEN

**HTTP 403** — The authenticated user does not have permission to perform this action.

**What to do:** The credential authenticated but is not allowed this action. Attach an IAM policy granting the action to the caller, or use a credential that has it.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#forbidden`

### FORCED_TOOL_CHOICE_CANNOT_STOP

**HTTP 400** — The agent forces tool use on every step (`tool_choice: "required"`, or the `{ type: "tool" }` object form) but declares no `has_tool_call` stop condition, so no turn it runs could ever end by answering — only by exhausting `max_steps`. Declare the terminal condition, or drop the forcing to `"auto"` and force a specific step with `step_rules` instead.

**What to do:** Add `{ "type": "has_tool_call", "tool_name": "<your done tool>" }` to `stop_conditions`, or set `tool_choice` to `"auto"` and force the step you actually care about with `step_rules`. `max_chain_generations` does not satisfy this — it bounds a chain, it never ends a turn.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#forced_tool_choice_cannot_stop`

### FORMATION_DELETE_FAILED

**HTTP 409** — One or more of the formation's resources could not be deleted, so the stack is left in `delete_failed`. `meta.failures` names each blocking resource (its logical id, type, and the underlying error) — the common case is a resource the platform refuses to delete on its own, such as an agent that has generation or trace history. Resolve those, then delete the formation again.

**What to do:** The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named in the message, then retry.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#formation_delete_failed`

### FORMATION_HANDLER_FAILED

**HTTP 502** — A resource type registered by the deployment operator delegates its lifecycle to an external HTTP handler, and that handler did not answer successfully — it returned a non-2xx status, timed out, was unreachable, or replied with a body the protocol does not allow (a create with no `physical_resource_id`, for instance). `meta.resource_type` and `meta.request_type` name the type and the operation, and the handler's own message is relayed when it sent one. This is an operator-side failure: the template is not at fault and re-deploying it unchanged will fail the same way until the handler is fixed.

**What to do:** A resource type this deployment registered delegates to an external handler, and that handler did not answer. The template is not at fault, so re-deploying it unchanged will fail identically — the deployment operator has to fix the handler (`meta.resource_type` names the type, `meta.request_type` the operation it failed on).

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#formation_handler_failed`

### FORMATION_INVALID_METADATA

**HTTP 400** — The formation-level `metadata` field contains a `sub`/`param`/`ref`/`ref_attr` substitution expression. That field is a static annotation bag and is never resolved; put deploy-time substitutions in the template top-level `metadata` block instead.

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#formation_invalid_metadata`

### FORMATION_MISSING_PARAMETERS

**HTTP 400** — The formation template declares one or more required parameters (no default, not satisfied by use_previous_value) that were not supplied or were provided as an empty string.

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#formation_missing_parameters`

### FORMATION_REPLACE_CLEANUP_FAILED

**HTTP 500** — A deploy replaced a resource — the handler answered a new `physical_resource_id` — and the superseded resource could not be deleted, so it is still live while the formation's ledger already points at the replacement. The deploy itself succeeded: the desired state is realised, and this is carried on the formation (and on its operation) rather than thrown, so it never appears as a response status. `meta.failures` names each un-deleted resource as `{ logical_id, resource_type, physical_resource_id, error }`. Each one stays on the formation as pending cleanup and is retried on the next deploy and on teardown; resolve whatever refused the delete and deploy again.

**What to do:** Retry with backoff. If it persists, the server log carries the detail the response deliberately omits.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#formation_replace_cleanup_failed`

### GENERATION_ALREADY_IN_PROGRESS

**HTTP 409** — A generation is already in progress for this session. Wait for it to complete before starting a new one.

**What to do:** A generation is already running for this target. Poll it to completion, or cancel it, before starting another.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#generation_already_in_progress`

### GENERATION_CONTENT_UNAVAILABLE

**HTTP 409** — The generation's content was never stored (the agent or project runs with trace_content_mode 'none') or has since been purged, so the turn cannot be replayed or curated.

**What to do:** The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named in the message, then retry.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#generation_content_unavailable`

### GENERATION_FAILED

**HTTP 500** — An unexpected error occurred during agent generation. Check the trace for details.

**What to do:** Retry with backoff. If it persists, the server log carries the detail the response deliberately omits.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#generation_failed`

### GENERATION_NOT_COMPLETED

**HTTP 409** — The generation has not completed, so it has no finished turn to promote. Only a completed generation can be curated into a dataset item.

**What to do:** The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named in the message, then retry.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#generation_not_completed`

### GENERATION_NOT_FOUND

**HTTP 404** — The generation does not exist or is not in a pending state for tool output submission.

**What to do:** Check the id and the project the credential can see, then retry. Listing the collection confirms whether the resource exists.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#generation_not_found`

### GUARDRAIL_HAS_REFERENCES

**HTTP 409** — The guardrail is still attached to one or more tools, agents, or projects (`guardrail_ids`) and cannot be deleted. Detach every reference first (requires guardrails:DetachGuardrail). The error meta lists the referencing resources by scope.

**What to do:** The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named in the message, then retry.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#guardrail_has_references`

### GUARDRAIL_NOT_FOUND

**HTTP 400** — A guardrail referenced by a `guardrail_ids` attachment does not exist in the project.

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#guardrail_not_found`

### HOOK_INVALID_JSON

**HTTP 400** — The inbound hook request body is not valid JSON.

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#hook_invalid_json`

### HOOK_PAYLOAD_TOO_LARGE

**HTTP 413** — The inbound hook request body exceeds the 1 MiB limit.

**What to do:** The payload exceeds the accepted size. Send less in one request, or use the upload flow for large bodies.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#hook_payload_too_large`

### INGESTION_CALLBACK_CONFLICT

**HTTP 409** — The document is no longer awaiting this conversion attempt — it already completed, timed out, or was superseded by a re-ingest.

**What to do:** The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named in the message, then retry.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#ingestion_callback_conflict`

### INGESTION_CALLBACK_INVALID_TOKEN

**HTTP 401** — The ingestion-callback token is missing, malformed, expired, or does not match the target document.

**What to do:** Authenticate the request: send a valid bearer token or project API key in the `Authorization` header.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#ingestion_callback_invalid_token`

### INGESTION_RULE_GLOB_CONFLICT

**HTTP 409** — An ingestion rule for this content_type_glob already exists in the project.

**What to do:** The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named in the message, then retry.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#ingestion_rule_glob_conflict`

### INGESTION_RULE_VALIDATION_FAILED

**HTTP 400** — The ingestion rule configuration is invalid (e.g. tool_id and agent_id are both set or both missing, the converter tool is a client tool, a soat/mcp converter tool is missing an action, content_type_glob is not a valid MIME type glob, or preset_parameters contains the reserved key "file" or "callback").

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#ingestion_rule_validation_failed`

### INTERNAL_ERROR

**HTTP 500** — An unhandled server-side failure. The message is always the constant "Internal Server Error" — the underlying exception is logged, never returned, so this error carries no detail a caller can act on beyond retrying.

**What to do:** Retry with backoff. The underlying exception is logged server-side and deliberately never forwarded, so the response carries nothing further to act on.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#internal_error`

### INVALID_CRON_EXPRESSION

**HTTP 400** — The cron expression is invalid. Triggers require a strict 5-field cron expression evaluated in UTC.

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#invalid_cron_expression`

### INVALID_EVENT_PATTERN

**HTTP 400** — The event pattern is invalid. An event trigger subscribes with '*', 'prefix.*', or an exact event name, and a pattern in a platform namespace must match a registered event.

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#invalid_event_pattern`

### INVALID_OUTPUT_SCHEMA

**HTTP 400** — The agent output_schema is invalid — it must be a JSON Schema object.

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#invalid_output_schema`

### INVALID_TEMPLATE_TOKEN

**HTTP 400** — A \{\{...\}\} double-curly token was found that is not a \{\{secret:sec_...\}\} or \{\{context:\<key\>\}\} reference, or a \{\{context:\<key\>\}\} token was found outside execute.headers / mcp.headers. Double curly braces are reserved for those two reference kinds; use single braces (\{param\}) for URL path parameters.

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#invalid_template_token`

### INVALID_TOOL_CONTEXT_KEY

**HTTP 400** — A tool_context key cannot be used as an HTTP header name. Keys are forwarded verbatim as \<prefix\>\<key\> headers, where the prefix is the deployment's TOOL_CONTEXT_HEADER_PREFIX (X-Soat-Context- by default), so a key may only contain letters, digits and the characters !#$%&'*+-.^_`|~, and two keys must not map to the same header name (header names are case-insensitive).

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#invalid_tool_context_key`

### MISSING_TOOL_CONTEXT_KEY

**HTTP 400** — A tool header references a \{\{context:\<key\>\}\} token whose key is not present in the tool_context for this call. The call is failed rather than sending the header with an empty value.

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#missing_tool_context_key`

### MODEL_LISTING_FAILED

**HTTP 502** — The provider rejected the model listing request, or answered it with something other than JSON. The provider's own status and message are carried in the error message.

**What to do:** An upstream dependency failed rather than this server itself. Retry with backoff and check that dependency.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#model_listing_failed`

### MODEL_LISTING_UNSUPPORTED

**HTTP 400** — The AI provider type cannot enumerate the models it can run. Azure lists deployments an operator named rather than models, and Ollama lists whatever was pulled onto that host, so neither answers the question.

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#model_listing_unsupported`

### MODEL_ROUTE_HAS_DEPENDENTS

**HTTP 409** — The model route is referenced by one or more agents, or is a project's default_model_route_id, and cannot be deleted. Repoint or delete the referencing consumers first; the error meta reports the counts and a sample of agent IDs.

**What to do:** The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named in the message, then retry.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#model_route_has_dependents`

### MODEL_ROUTE_NOT_FOUND

**HTTP 400** — A referenced model route does not exist in the project.

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#model_route_not_found`

### NAME_CONFLICT

**HTTP 409** — A resource with this name already exists in the project. Use a different name.

**What to do:** A resource with this name already exists in the project. Pick another name, or update the existing resource instead of creating a second one.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#name_conflict`

### NOT_IMPLEMENTED

**HTTP 501** — The request is well-formed but names an option this API version does not implement.

**What to do:** The operation exists in the contract but is not implemented on this deployment. Retrying will not change the outcome.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#not_implemented`

### NO_ACTIVE_RELEASE

**HTTP 409** — The agent has no active release to promote or abort. Set one with PUT /agents/\{agent_id\}/release first.

**What to do:** The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named in the message, then retry.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#no_active_release`

### ORCHESTRATION_CYCLE_DETECTED

**HTTP 422** — The orchestration graph contains a cycle. Cycles are not supported.

**What to do:** The graph has a cycle, so no topological order exists. Break the cycle named in `meta` — a loop belongs in a workflow, not a DAG.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#orchestration_cycle_detected`

### ORCHESTRATION_DISPATCH_FAILED

**HTTP 422** — A workflow on_enter dispatch's orchestration run settled in a non-success terminal status (failed, cancelled, or expired).

**What to do:** The request was accepted and then could not be carried out. The failure is in the work, not the request shape — read `meta` and the trace for which step failed before retrying.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#orchestration_dispatch_failed`

### ORCHESTRATION_HUMAN_NODE_MISMATCH

**HTTP 400** — The specified node is not the active human node waiting for input.

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#orchestration_human_node_mismatch`

### ORCHESTRATION_MAX_ITERATIONS_EXCEEDED

**HTTP 422** — The orchestration run exceeded the maximum number of iterations.

**What to do:** The request was accepted and then could not be carried out. The failure is in the work, not the request shape — read `meta` and the trace for which step failed before retrying.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#orchestration_max_iterations_exceeded`

### ORCHESTRATION_NESTED_RUN_FAILED

**HTTP 422** — A `loop` / `sub_orchestration` child run settled in a non-success terminal status (failed, cancelled, or expired) carrying no code of its own, so the node that started it failed too. A child that carries a code fails its parent under that code instead, so the cause reaches the run a caller reads (#1185). Matches ORCHESTRATION_DISPATCH_FAILED, the same rule for a workflow on_enter dispatch's run.

**What to do:** The request was accepted and then could not be carried out. The failure is in the work, not the request shape — read `meta` and the trace for which step failed before retrying.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#orchestration_nested_run_failed`

### ORCHESTRATION_NODE_FAILED

**HTTP 422** — A node in the orchestration run failed to execute.

**What to do:** The request was accepted and then could not be carried out. The failure is in the work, not the request shape — read `meta` and the trace for which step failed before retrying.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#orchestration_node_failed`

### ORCHESTRATION_NOT_FOUND

**HTTP 404** — The orchestration does not exist or is not accessible.

**What to do:** Check the id and the project the credential can see, then retry. Listing the collection confirms whether the resource exists.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#orchestration_not_found`

### ORCHESTRATION_POLL_EXHAUSTED

**HTTP 422** — A poll node reached its attempt limit without its exit condition becoming true (only raised when fail_on_timeout is set).

**What to do:** The request was accepted and then could not be carried out. The failure is in the work, not the request shape — read `meta` and the trace for which step failed before retrying.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#orchestration_poll_exhausted`

### ORCHESTRATION_RUN_DEPTH_LIMIT

**HTTP 409** — Starting the next `loop` / `sub_orchestration` child run would nest past the depth bound, so it was refused. This bounds a cycle no intra-graph validator can see — a graph whose `sub_orchestration` node names itself, directly or through a cycle of two graphs — which the orchestration cycle validator (intra-graph, and excluding loop nodes deliberately) cannot detect. The bound is the smaller of MAX_ORCHESTRATION_RUN_DEPTH (default 10) and the project's `max_run_depth`; the error meta names the depth reached, the limit, and which of the two set it (#1185).

**What to do:** Nesting is recursing: follow `parent_orchestration_run_id` up from the failed run to find the `sub_orchestration` (or `loop`) node that names a graph already in the chain, and repoint it. If the composition is legitimately this deep, raise the project bound with `PATCH /api/v1/projects/{project_id}` (`max_run_depth`) — `meta.limit_source` says whether the project or the deployment set the number that refused it.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#orchestration_run_depth_limit`

### ORCHESTRATION_RUN_NOT_AWAITING_INPUT

**HTTP 409** — The orchestration run is not awaiting input.

**What to do:** The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named in the message, then retry.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#orchestration_run_not_awaiting_input`

### ORCHESTRATION_RUN_NOT_CANCELLABLE

**HTTP 409** — The orchestration run is already in a terminal state and cannot be cancelled.

**What to do:** The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named in the message, then retry.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#orchestration_run_not_cancellable`

### ORCHESTRATION_RUN_NOT_FOUND

**HTTP 404** — The orchestration run does not exist or is not accessible.

**What to do:** Check the id and the project the credential can see, then retry. Listing the collection confirms whether the resource exists.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#orchestration_run_not_found`

### ORCHESTRATION_VALIDATION_FAILED

**HTTP 400** — The orchestration graph is invalid (e.g. a dangling edge, a node missing a required field, a cycle, or an input_mapping that references a state key no upstream node writes).

**What to do:** The orchestration graph is not runnable as declared. `meta` names the failing node or edge; `POST /api/v1/orchestrations/{orchestration_id}/validate` reports every problem at once.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#orchestration_validation_failed`

### OUTPUT_SCHEMA_STREAMING_UNSUPPORTED

**HTTP 400** — Streaming generation does not support output_schema. Set stream to false, or remove output_schema from the agent/request.

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#output_schema_streaming_unsupported`

### OUTPUT_SCHEMA_VALIDATION_FAILED

**HTTP 502** — The model returned an object that does not satisfy the agent output_schema (or was not valid JSON at all). Upstream-caused like AI_PROVIDER_ERROR: the request was well-formed, the model output was not.

**What to do:** An upstream dependency failed rather than this server itself. Retry with backoff and check that dependency.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#output_schema_validation_failed`

### PDF_PARSE_FAILED

**HTTP 400** — The uploaded file could not be parsed as a PDF. Ensure the file is a valid PDF document.

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#pdf_parse_failed`

### PIPELINE_DEPTH_EXCEEDED

**HTTP 422** — The pipeline tool exceeded the maximum nested execution depth (pipelines calling pipelines).

**What to do:** The request was accepted and then could not be carried out. The failure is in the work, not the request shape — read `meta` and the trace for which step failed before retrying.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#pipeline_depth_exceeded`

### PIPELINE_INVALID_STEP

**HTTP 400** — The pipeline tool configuration is invalid (e.g. no steps, a duplicate or malformed step id, a missing tool reference, a forward reference to a later step, or a step that targets a client tool).

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#pipeline_invalid_step`

### PIPELINE_STEP_FAILED

**HTTP 422** — A step in the pipeline tool failed to execute.

**What to do:** The request was accepted and then could not be carried out. The failure is in the work, not the request shape — read `meta` and the trace for which step failed before retrying.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#pipeline_step_failed`

### POLICY_NOT_FOUND

**HTTP 400** — A referenced policy does not exist.

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#policy_not_found`

### PROJECT_DEFAULT_ROUTE_INHERITED

**HTTP 409** — A project's default_model_route_id cannot be cleared while consumers that bind neither an AI provider nor a model route inherit it — clearing it would leave them with no resolvable model. Repointing the default to another route is always allowed; the error meta reports the count and a sample of inheriting resource IDs.

**What to do:** The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named in the message, then retry.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#project_default_route_inherited`

### PROJECT_HAS_DEPENDENTS

**HTTP 409** — The project has one or more dependent resources (agents, ai providers, tools, etc.) and cannot be deleted. Use force=true to delete dependents as well.

**What to do:** The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named in the message, then retry.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#project_has_dependents`

### PROMOTION_GATE_UNMET

**HTTP 409** — The release's promotion_gate has no passing eval run against the canary version. Run the eval with agent_version pinned to the canary, or abort the rollout.

**What to do:** The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named in the message, then retry.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#promotion_gate_unmet`

### QUEUE_DRIVER_MISCONFIGURED

**HTTP 500** — The orchestration queue driver is not configured correctly — an unknown ORCHESTRATION_QUEUE_DRIVER, or a missing required variable for the selected driver (e.g. ORCHESTRATION_QUEUE_SQS_QUEUE_URL for sqs).

**What to do:** Retry with backoff. If it persists, the server log carries the detail the response deliberately omits.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#queue_driver_misconfigured`

### QUOTA_CONFLICT

**HTTP 409** — A quota with the same (project, scope, scope_ref, metric, window) already exists. The all-enforce precedence rule makes duplicates pure redundancy, so a duplicate is rejected instead of stored.

**What to do:** The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named in the message, then retry.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#quota_conflict`

### QUOTA_EXCEEDED

**HTTP 429** — An enforced quota has been exceeded for the request scope. The response carries a `Retry-After` header (seconds until the window resets) and a `meta` block naming the breached quota, metric, limit, window, and reset time.

**What to do:** Wait until the window resets — `Retry-After` (seconds) and `meta.resets_at` both carry the time — or raise the quota with `PATCH /api/v1/quotas/{quota_id}`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#quota_exceeded`

### QUOTA_UNENFORCEABLE

**HTTP 409** — An enforced `cost_usd` quota with `on_unpriced: "block"` (the default) cannot be evaluated: the current window holds a pricing blackout — several metered events, none of them priced — so the aggregate is `0` however much was actually spent. The cap refuses the generation rather than waving through spend it cannot measure. No `Retry-After` is sent — the window resetting changes nothing; configure pricing for the models in use, or set the quota's `on_unpriced` to `"allow"` to accept unmeasurable spend explicitly.

**What to do:** Add price book entries covering the models this project runs (`POST /api/v1/prices`), so the cost quota has something to aggregate. To accept unmeasurable spend instead, set `on_unpriced: "allow"` on the quota with `PATCH /api/v1/quotas/{quota_id}` (or switch it to `monitor` mode to stop blocking entirely).

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#quota_unenforceable`

### REQUEST_REJECTED

**HTTP 400** — The HTTP framework rejected the request before it reached a handler — a payload over the size limit, an unsupported method, a malformed request line. The accompanying message carries the specific reason, and the response status is the one the framework chose rather than this default.

**What to do:** The HTTP layer rejected the request before it reached a route — method, content type, or size. The message names the reason.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#request_rejected`

### RESOURCE_NOT_FOUND

**HTTP 404** — The requested resource does not exist or is not accessible.

**What to do:** Check the id, and check that the credential can see the project that owns the resource — a resource in another project is indistinguishable from one that does not exist. List the collection to confirm.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#resource_not_found`

### SECRET_HAS_DEPENDENTS

**HTTP 409** — The secret is referenced by one or more AI providers and cannot be deleted. Use force=true to delete dependents as well.

**What to do:** The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named in the message, then retry.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#secret_has_dependents`

### SECRET_NOT_DECRYPTABLE

**HTTP 500** — A stored trigger or webhook secret could not be decrypted — SECRETS_ENCRYPTION_KEY has changed since it was written. Rotating the secret replaces it; restoring the original key recovers every secret written under it.

**What to do:** Retry with backoff. If it persists, the server log carries the detail the response deliberately omits.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#secret_not_decryptable`

### SECRET_NOT_FOUND

**HTTP 400** — A secret referenced by a \{\{secret:...\}\} token does not exist in this project.

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#secret_not_found`

### SESSION_CLOSED

**HTTP 409** — The session is closed and does not accept new messages or generation requests. Open a new session to continue.

**What to do:** The session is closed and accepts no further messages. Open a new one, or fork this one to keep its history.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#session_closed`

### SESSION_EXPIRED

**HTTP 410** — The session has expired due to inactivity. Open a new session to continue.

**What to do:** Open a new session; an expired one cannot be revived. Raise the agent's session TTL if this happens sooner than the workload needs.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#session_expired`

### SINGLE_SESSION_CONFLICT

**HTTP 409** — An open session already exists for this actor. Use the existing session or close it first.

**What to do:** The agent allows one open session per actor. Reuse the session named in `meta.session_id`, or close it first.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#single_session_conflict`

### SYSTEM_MESSAGE_NOT_ALLOWED

**HTTP 400** — A `role: "system"` entry was supplied in `messages`. System content never travels as a message on any SOAT surface — it goes in the `instructions` field instead — the same name on every surface (a completion request, a Chat, an Agent). Mirrors the AI SDK, whose `allowSystemInMessages` defaults to false because a system entry in a caller-supplied array is a prompt-injection vector.

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#system_message_not_allowed`

### TASK_AUTOMATION_CHAIN_LIMIT

**HTTP 409** — The task has run too many machine-driven transitions back-to-back with no outside intervention, and the next one was refused. This bounds a cycle composed across modules — a workflow state dispatching work that transitions the task back into that same state — which neither the orchestration cycle validator (intra-graph) nor the workflow itself (where revisiting states is deliberate) can see. Any move by a person, a plain API key, or an approval resolution resets the chain. The ceiling is TASK_AUTOMATION_CHAIN_LIMIT (default 50) (#885).

**What to do:** The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named in the message, then retry.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#task_automation_chain_limit`

### TASK_AUTOMATION_PROVENANCE_MISSING

**HTTP 500** — An automation-caused transition was about to be persisted with no recorded cause: principal_id, generation_id, and orchestration_run_id would all be null. This is a writer bug, not a valid degraded record — the transition is rejected instead of being silently written (#792).

**What to do:** Retry with backoff. If it persists, the server log carries the detail the response deliberately omits.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#task_automation_provenance_missing`

### TASK_GUARD_REJECTED

**HTTP 400** — The transition guard evaluated to false for this task.

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#task_guard_rejected`

### TASK_NOT_FOUND

**HTTP 404** — The task does not exist or is not accessible.

**What to do:** Check the id and the project the credential can see, then retry. Listing the collection confirms whether the resource exists.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#task_not_found`

### TASK_PAYLOAD_INVALID

**HTTP 400** — The task payload does not satisfy the workflow's payload_schema.

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#task_payload_invalid`

### TASK_STATE_NOT_FOUND

**HTTP 400** — A task create request's `state` does not name a declared state of the workflow.

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#task_state_not_found`

### TASK_TRANSITION_CONFLICT

**HTTP 409** — A concurrent change made the requested transition invalid from the current state, or the task is already closed.

**What to do:** The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named in the message, then retry.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#task_transition_conflict`

### TASK_TRANSITION_NOT_FOUND

**HTTP 400** — The named transition does not exist in the workflow.

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#task_transition_not_found`

### TEXT_ENCODED_TOOL_CALL

**HTTP 502** — The model wrote a tool invocation as plain assistant text (e.g. a ```json block containing \{"name": "\", "arguments": \{…\}\}) instead of making a structured tool call, so the tool never ran. Upstream-caused like AI_PROVIDER_ERROR: returning that text as the answer would be a silent data-integrity failure, so the generation fails instead.

**What to do:** An upstream dependency failed rather than this server itself. Retry with backoff and check that dependency.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#text_encoded_tool_call`

### TOOL_AUTH_FAILED

**HTTP 502** — An http-type tool's `execute.auth` credentials could not be turned into a request credential — malformed service account JSON, an unusable private key, or a token endpoint that rejected the assertion. The error `meta` carries the token endpoint's status and body when it responded. Distinct from `TOOL_HTTP_ERROR`, which is the tool's own target rejecting the call.

**What to do:** An upstream dependency failed rather than this server itself. Retry with backoff and check that dependency.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#tool_auth_failed`

### TOOL_CALL_NOT_SUPPORTED

**HTTP 422** — This tool type cannot be invoked server-side. Client tools must be executed by the calling client.

**What to do:** The request was accepted and then could not be carried out. The failure is in the work, not the request shape — read `meta` and the trace for which step failed before retrying.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#tool_call_not_supported`

### TOOL_DISPATCH_FAILED

**HTTP 422** — A workflow on_enter `tool` dispatch did not return a result. The tool call was settled before it ran — blocked by a guardrail (class D or a class-B tripwire), or routed to human approval (class C), which a task dispatch cannot park on. The error `meta` carries the `tool_id` and the `outcome` that settled it. Dispatch an orchestration instead when the tool is approval-gated: its engine can park and resume.

**What to do:** The request was accepted and then could not be carried out. The failure is in the work, not the request shape — read `meta` and the trace for which step failed before retrying.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#tool_dispatch_failed`

### TOOL_EGRESS_BLOCKED

**HTTP 403** — An http- or mcp-type tool's target is not publicly routable — a loopback, private, link-local (cloud metadata), CGNAT or IPv6 ULA address — and the deployment's TOOL_EGRESS_ALLOWED_HOSTS does not list it. Also returned when a hostname resolves to such an address, when a redirect leads to one, when the scheme is not http/https, or when the redirect chain is too long. The error `meta` carries `tool_url` and, when known, the offending `tool_address`.

**What to do:** The credential is authenticated but lacks this action, or is scoped to another project. Grant the action in an IAM policy, or use a credential that has it.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#tool_egress_blocked`

### TOOL_HTTP_ERROR

**HTTP 502** — An http-type tool call returned a non-2xx response. The error `meta` carries the real upstream status code, response body, URL, and method.

**What to do:** An upstream dependency failed rather than this server itself. Retry with backoff and check that dependency.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#tool_http_error`

### TOOL_NOT_FOUND

**HTTP 400** — A referenced tool does not exist.

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#tool_not_found`

### TRIGGER_ACTION_NOT_ALLOWED

**HTTP 400** — A trigger field is incompatible with its type: `action` is only valid for tool targets (and required for soat/mcp tools), and `cron` is required for schedule triggers and rejected otherwise.

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#trigger_action_not_allowed`

### TRIGGER_CAUSATION_LIMIT

**HTTP 409** — An event trigger refused to extend the causal chain that reached it — it is already in the chain, or the chain has run too deep.

**What to do:** The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named in the message, then retry.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#trigger_causation_limit`

### TRIGGER_CREATOR_UNAVAILABLE

**HTTP 409** — The trigger creator no longer exists, so the firing identity cannot be resolved.

**What to do:** The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named in the message, then retry.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#trigger_creator_unavailable`

### TRIGGER_INPUT_INVALID

**HTTP 400** — The effective input is invalid for the target (empty agent input, or an orchestration input_schema violation).

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#trigger_input_invalid`

### TRIGGER_NOT_ACTIVE

**HTTP 409** — The trigger is inactive and cannot fire.

**What to do:** The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named in the message, then retry.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#trigger_not_active`

### TRIGGER_RECURSION_FORBIDDEN

**HTTP 403** — A trigger-scoped credential cannot fire a trigger, preventing unbounded trigger→trigger loops.

**What to do:** The credential is authenticated but lacks this action, or is scoped to another project. Grant the action in an IAM policy, or use a credential that has it.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#trigger_recursion_forbidden`

### TRIGGER_TARGET_NOT_FOUND

**HTTP 400** — The trigger target does not exist in the project, or its type does not match target_type.

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#trigger_target_not_found`

### TRIGGER_TYPE_IMMUTABLE

**HTTP 400** — A trigger’s type cannot be changed after creation.

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#trigger_type_immutable`

### UNAUTHORIZED

**HTTP 401** — Authentication is required or the token is invalid.

**What to do:** Send `Authorization: Bearer <token>` — a user token from `POST /api/v1/users/login`, a project API key (`sk_…`), or an OAuth access token. Discovery metadata for the OAuth flow is at `/.well-known/oauth-authorization-server`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#unauthorized`

### UNSUPPORTED_FILE_TYPE

**HTTP 400** — The referenced file has a content type that cannot be ingested. Supported types: application/pdf, text/plain, text/markdown.

**What to do:** The file's media type has no parser on this deployment. Convert it to a supported type, or add an ingestion rule that does.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#unsupported_file_type`

### UPLOAD_TOKEN_EXPIRED

**HTTP 410** — The upload token has expired. Request a new one.

**What to do:** Request a fresh upload token; tokens are single-use and short-lived by design.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#upload_token_expired`

### UPLOAD_TOKEN_NOT_FOUND

**HTTP 404** — The upload token does not exist.

**What to do:** Check the id and the project the credential can see, then retry. Listing the collection confirms whether the resource exists.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#upload_token_not_found`

### UPLOAD_TOKEN_USED

**HTTP 409** — The upload token has already been used.

**What to do:** This upload token has already been redeemed. Request a new one for another upload.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#upload_token_used`

### VALIDATION_FAILED

**HTTP 400** — The request body failed validation.

**What to do:** Fix the request and retry. Unknown fields are rejected outright, so compare the payload against the operation in `/openapi.json`; `meta` names the offending field when the check can identify one.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#validation_failed`

### WORKFLOW_HAS_OPEN_TASKS

**HTTP 409** — The workflow has one or more open tasks and cannot be deleted.

**What to do:** The request is well-formed but the resource is not in a state that allows it. Resolve the conflicting state named in the message, then retry.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#workflow_has_open_tasks`

### WORKFLOW_NOT_FOUND

**HTTP 404** — The workflow does not exist or is not accessible.

**What to do:** Check the id and the project the credential can see, then retry. Listing the collection confirms whether the resource exists.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#workflow_not_found`

### WORKFLOW_VALIDATION_FAILED

**HTTP 400** — The workflow definition is invalid (e.g. duplicate state names, not exactly one initial state, a transition referencing an unknown state, a malformed guard, or an on_enter dispatch missing its target).

**What to do:** Fix the request and retry — the payload, parameters, or referenced ids are invalid. Compare the request against the operation in `/openapi.json`.

Reached by `docs_url`: `https://soat.ttoss.dev/docs/error-codes#workflow_validation_failed`
