Tool Execution Errors
Every TOOL_VENDOR_ code a governed extension tool can return, what caused it, and whether retrying helps.
When an extension tool fails, Scrydon returns a stable TOOL_VENDOR_* code
rather than the underlying error. This page lists every code, what causes it,
and what to do about it.
What a failure looks like
A failed tool call returns:
{
"success": false,
"output": {},
"error": "TOOL_VENDOR_EGRESS_DENIED",
"message": "The destination is not on the approved egress list for this connection.",
"metadata": { "retryable": false }
}In a workflow execution the same code appears against the node that failed:
{ "failedNodes": 1, "errors": ["b40d978e-…: TOOL_VENDOR_EGRESS_DENIED"] }Branch on error. message is a human-readable sentence for that code, meant
for whoever reads the failure — it is stable per code but may be reworded, so it
is not a branch key. It is selected by the code and never interpolates the
underlying reason, so it tells you nothing the code did not already.
The detail is withheld on purpose. Extension tools run inside a sandboxed microVM on your data, with credentials your organization owns. Raw vendor errors, credential material, network detail, and DLP findings never cross back to the caller, because the caller is often a model. You get a stable code; the full reason stays server-side.
Policy blocked the call
Governance refused the request. These are decisions, not faults — retrying an identical request always fails the same way.
| Code | Cause | What to do |
|---|---|---|
TOOL_VENDOR_NOT_SUPPORTED | The archive has no implementation for the hook the platform asked for (for example a model-discovery fetcher the vendor never declared) | Nothing to retry: the platform treats it as an empty answer. If a tool you expect to exist returns it, the archive does not export it |
TOOL_VENDOR_EGRESS_DENIED | The destination is not on the approved egress list for this connection | Add the host to the connection's allowed destinations |
TOOL_VENDOR_DESTINATION_DENIED | The URL the tool built is malformed, or its host is not allowed | Check the tool's inputs; then the connection's allowed hosts |
TOOL_VENDOR_REDIRECT_DENIED | The destination redirected somewhere policy does not allow | Allow the redirect target, or point the tool at the final URL |
TOOL_VENDOR_DLP_BLOCKED | Outbound DLP matched the request the tool actually emitted | Remove or reclassify the blocked data, or amend DLP policy |
TOOL_VENDOR_RESPONSE_SECRET_BLOCKED | The vendor's response contained credential-shaped data and was withheld | Expected when a vendor echoes secrets; investigate if unexpected |
TOOL_VENDOR_HEADER_DENIED | The tool set a forbidden or proxy-* header | Fix the tool; these headers are owned by the egress broker |
TOOL_VENDOR_CREDENTIAL_PLACEMENT_DENIED | A credential was placed somewhere its auth mode disallows — for example a header secret in a body | Correct the tool's auth-mode placement |
TOOL_VENDOR_UNKNOWN_CREDENTIAL_PLACEHOLDER | The request still held a scrydon_secret_* placeholder with nothing bound to it | Bind every placeholder the tool references, or remove it |
TOOL_VENDOR_CLI_NOT_CONFIGURED | The requested CLI id is not declared by this extension's manifest | Check the CLI id against the extension's declared clis |
TOOL_VENDOR_CLI_IDENTITY_NOT_CONFIGURED | This CLI needs an application (service-principal) identity this deployment has not configured | Configure the organization's service-principal credentials for this extension before using a credentialHosts/bootstrap CLI |
TOOL_VENDOR_CLI_NO_BINDABLE_CREDENTIAL | The CLI's credential could not be bound to any of its granted egress hosts — including a CLI with endpointEnv whose organization configured more than one endpoint | Grant the CLI at least one non-wildcard host its credential can bind to; for endpointEnv, configure exactly one endpoint |
TOOL_VENDOR_CLI_ISOLATION_REQUIRED | A CLI dispatch reached execution without a microVM tier | Internal; report it if you see it — CLI dispatch is supposed to always admit into microVM |
TOOL_VENDOR_CLI_INVALID_INPUT | The CLI dispatch's input did not match the runner's expected { args, stdin? } argv shape | Check the tool call's arguments against the CLI's declared invocation shape |
TOOL_VENDOR_CLI_QUERY_PARAM_CREDENTIAL | This CLI's credential is configured for query-parameter delivery, which stamping cannot deliver | Reconfigure the credential for header-based delivery, or contact support |
TOOL_VENDOR_CLI_INVALID_CREDENTIAL_HOSTS | The CLI's declared credentialHosts names a host outside the granted egress set, a token-emulation-only host, a wildcard, or the same host twice | Fix the extension manifest's credentialHosts declaration |
TOOL_VENDOR_CLI_CREDENTIAL_HOSTS_REQUIRED | The CLI declares no credentialHosts, and the extension's own egress list has more than one host (an endpoint the organization configured is not counted), so which host the credential belongs to is not guessed | Add a hosts-only credentialHosts entry naming the hosts the connection's credential is valid for |
TOOL_VENDOR_CLI_DISALLOWED_PREAMBLE_PLACEHOLDER | The CLI's bootstrap.preamble used a placeholder outside the allowed set, or a malformed one | Fix the extension manifest's bootstrap.preamble template |
EXEC_ACTION_CONFIRMATION_DECLINED | A person declined the destructive CLI request this call needed approval for. Nothing was re-run or sent, and the agent continues its step | Nothing to retry: repeating the call asks the person again. See Approving a deletion |
CLI_CONFIRMATION_AFTER_WRITE | A CLI command made a destructive request after an earlier request in the same command had already been sent as a write. No one was asked, because approving would re-run the command and send that write again; the destructive request was not sent | Run the destructive command on its own, as a separate call. Check the earlier write's result before repeating it: its outcome is unknown. See Approving a deletion |
A CLI call on a non-reviewed extension never reaches this table. Only
first-party, reviewed Scrydon releases may declare a CLI (v1.0.0). Dispatching
one against an org-installed extension is refused before execution starts, as
CAPABILITY_NOT_CONFIGURED — the platform's general "this capability isn't
available" code, not a TOOL_VENDOR_* result.
Limits and deadlines
| Code | Cause | Retry? |
|---|---|---|
TOOL_VENDOR_TIMEOUT | The invocation exceeded its deadline | Yes, if the operation is safe to repeat |
TOOL_VENDOR_EGRESS_TIMEOUT | A single outbound request exceeded its deadline | Yes, same caveat |
TOOL_VENDOR_EGRESS_BUDGET_EXHAUSTED | The tool made more outbound requests than its budget allows | No — reduce the tool's request count |
TOOL_VENDOR_REQUEST_TOO_LARGE | The outbound request exceeded the size cap | No — send less |
TOOL_VENDOR_RESULT_TOO_LARGE | The tool's result exceeded the size cap | No — return less, or paginate |
TOOL_VENDOR_CANCELLED | The parent execution was cancelled | No — start a new execution |
TOOL_VENDOR_EGRESS_CANCELLED | An outbound request was cancelled mid-flight | No |
A timeout does not tell you whether the request reached the destination. For anything with side effects, reconcile before retrying rather than assuming it did not commit.
The tool or its archive failed
| Code | Cause | What to do |
|---|---|---|
TOOL_VENDOR_EXECUTION_FAILED | The tool threw, or a failure with no more specific code | See Getting the real reason — this is the catch-all |
TOOL_VENDOR_PROTOCOL_INVALID | The archive's request, result, or log framing was malformed or out of bounds | Fix or update the code release |
TOOL_VENDOR_ARTIFACT_HASH_MISMATCH | Archive bytes did not match the digest pinned for this execution | Reinstall the extension; if it recurs, treat as tampering |
TOOL_VENDOR_INVOCATION_BINDING_MISMATCH | The invocation did not match the artifact it was bound to | Reinstall the extension and retry |
TOOL_VENDOR_ALREADY_INVOKED | A single-use sandbox received a second invocation | No action — the sandbox is single-use by design |
TOOL_VENDOR_UNAUTHORIZED | The sandbox rejected an unauthenticated invocation | Internal; report it if you see it |
TOOL_VENDOR_REQUEST_INVALID | The outbound request the tool built is malformed — an invalid body encoding, or a body on a GET/HEAD | Fix the tool's request construction |
TOOL_VENDOR_EGRESS_PROTOCOL_INVALID | The sandbox and the egress broker disagreed on framing | Internal; report it if you see it |
TOOL_VENDOR_CONTEXT_INVALID | Credentials could not be projected into the sandbox for this call | Recheck the connection's credentials and bindings |
TOOL_VENDOR_EXECUTION_FAILED is also what any unrecognized code collapses
to, so it is the code you are most likely to see and the least specific. It does
not by itself mean the tool's own logic failed.
The platform could not run it
These are infrastructure conditions. On Scrydon Cloud they are ours to fix; if you self-host, they point at your deployment.
| Code | Cause |
|---|---|
TOOL_VENDOR_ISOLATION_UNAVAILABLE | The sandbox could not be provisioned — no isolation capacity, or the pod could not be scheduled |
TOOL_VENDOR_ATTESTATION_REQUIRED | The vendor execution boundary has no current proof, so execution refuses rather than downgrading |
TOOL_VENDOR_EGRESS_UNAVAILABLE | The egress broker could not be attached to the sandbox |
TOOL_VENDOR_BROKER_NOT_READY | The tool called out before its egress grant was installed, or after the call's audit trail was sealed |
TOOL_VENDOR_EGRESS_FAILED | Outbound transport failed before a response |
TOOL_VENDOR_MANAGED_EXECUTION_UNAVAILABLE | The managed-process execution tier was unreachable |
TOOL_VENDOR_GRANT_UNAVAILABLE | The sandbox could not fetch its one-use grant |
TOOL_VENDOR_GRANT_INVALID | The grant it fetched did not validate |
TOOL_VENDOR_GRANT_TIMEOUT | The grant fetch exceeded its deadline — while connecting, waiting for a ready grant, or reading the grant body |
TOOL_VENDOR_GRANT_EXPIRED | The grant's own deadline passed before the sandbox redeemed it |
TOOL_VENDOR_GRANT_INSTALL_FAILED | The grant install failed for a reason outside the four above |
TOOL_VENDOR_GRANT_STORE_UNAVAILABLE | The grant store was unreachable |
TOOL_VENDOR_DLP_POLICY_UNAVAILABLE | DLP policy could not be loaded, so the call failed closed |
TOOL_VENDOR_EGRESS_POLICY_UNAVAILABLE | Egress policy could not be loaded, so the call failed closed |
TOOL_VENDOR_CLEANUP_FAILED | The sandbox or its grants could not be proven torn down |
TOOL_VENDOR_PLATFORM_SDK_UNAVAILABLE | The tool declares platformOperations, and its microVM execution could not be granted the platform SDK callback; nothing ran |
TOOL_VENDOR_EXEC_ID_REQUIRED | A pre-started sandbox was sent the call without the execution it belongs to, and refused it before running anything |
TOOL_VENDOR_EXEC_ID_CONFLICT | The sandbox was already bound to a different execution, and refused this one before running anything |
TOOL_VENDOR_AUDIT_SEAL_PENDING | The egress broker could not finish writing the call's audit trail within its bound. Not a failure of the call: the platform waits for the sandbox to exit and seals the trail then |
The two *_POLICY_UNAVAILABLE codes are deliberate: when governance policy
cannot be loaded, the call is refused rather than run ungoverned.
Whether the call was sent
When an agent calls a tool from its sandbox, these codes tell the agent whether the call reached the tool. Each is recorded against the call's id. Presenting the same call id again returns the same answer and never sends the call.
| Code | Cause | What to do |
|---|---|---|
EXEC_ACTION_NOT_DISPATCHED | The platform refused the call before sending it. It could not read the classification of what this agent turn has already read, and it never sends a call it cannot classify. Nothing was sent, so the call had no effect. retryable is true only when the cause was temporary: the platform's state store timed out, could not be reached, or answered with a server error. A lasting cause is retryable: false: the state store is not configured, or the stored record is unreadable | When retryable is true, retry as a new call. The new call is checked from the start, so a call that needs approval asks a person again: an approval given for the refused call does not carry over. When retryable is false, do not retry, because the same cause refuses it again. On Scrydon Cloud that cause is ours to fix. If you self-host, check the agentic service's Dapr state store |
EXEC_ACTION_EXECUTION_GRANT_REQUIRED | The call was made for a user and is gated at a classification, but the run holds no execution grant to act under. A call made for a user may not also assert a classification, so the platform refused it before sending it. Nothing was sent, so the call had no effect. Always retryable: false | Do not retry: the same call is refused the same way. Start the run so that it carries an execution grant (a run started by a signed-in user does), or run the tool at no classification |
POLICY_BLOCKED, FORBIDDEN, CAPABILITY_NOT_CONFIGURED | The platform refused the call before running the tool: the tool's endpoint is not rated for the classification this turn has read (the message starts with EGRESS_BLOCKED_BY_CLEARANCE), the run is not allowed to use the tool, the connected account cannot reach the resource the call names, or the tool is not configured for this tenant. To decide the resource case the platform may first ask the vendor, with a read-only check, whether the account can reach that resource; the call itself is never sent. The tool never ran, so the call had no effect. retryable is true only when the cause was temporary: today that is a CAPABILITY_NOT_CONFIGURED raised because the platform could not look up which account the call runs as on the connection (the acting principal's own account, or the connection's default account). Every other refusal here is retryable: false. A POLICY_BLOCKED raised for any other reason (a DLP or moderation block, a CLI request refused by its tier) may come after the call was sent, so it is reported as EXEC_ACTION_EFFECT_UNKNOWN or with its own CLI code instead | When retryable is true, retry as a new call; the tool never ran the first time. When it is false, do not retry the same call: it is refused the same way. For EGRESS_BLOCKED_BY_CLEARANCE, an administrator rates the endpoint under Settings → Governance → Identity → Endpoints → Endpoint ratings, or the run asserts a lower classification. For the other two, check the tool's installation, entitlement and connection |
EXEC_ACTION_EFFECT_UNKNOWN | The call may have reached the tool, but its outcome was never recorded. For example, the connection failed after the request left, or the result could not be classified. The side effect may have happened. Always retryable: false | Do not repeat the call blindly. First check with the tool's read operation whether the effect happened, then decide. Presenting the same call id again returns this code and sends nothing |
When to retry
Do not infer retryability from the code. Read metadata.retryable — it is set
from the failure's transport characteristics, so the same code can be retryable
in one case and not in another.
As a rule:
- Never retry a policy denial. The decision is deterministic.
- Retry with backoff on capacity and transport conditions, and only when the operation is safe to repeat.
- Never auto-retry a side-effecting call after a timeout without reconciling first — a timeout does not prove the request did not commit.
- Wait at least
retryAfterseconds when the failure carries one. It is the provider's own requested back-off, and a retry that lands sooner meets the same limit.
In your extension, report a non-OK provider response with
throwExtensionHttpError(response) from
@scrydon/sdk-authoring/extensions/extension-failure rather than a bare
Error. It carries the HTTP status and the provider's error code and, when the
provider sent a retry-after-ms or retry-after header, its back-off as
retryAfter in whole seconds. The platform forwards that value as the error's
retryAfter and the response's Retry-After header — a bare Error loses all
three, so every caller can only guess.
What the platform already did before answering you
The platform applies that last rule to itself, so the failure you receive is not the first thing that went wrong — but for a tool call, it is the only thing.
Each platform operation declares whether it is safe to repeat. For a read-only one, a transient provider failure is retried internally (bounded, with backoff, against the same provider — a retry is never permission to switch vendors), so a failure that reaches you has already survived those attempts.
Tool execution is not read-only. A governed tool call can file a ticket, post a message, or create a record, so the platform makes exactly one attempt and hands you the failure. The same applies to creating, renewing, or deleting a webhook subscription. Two consequences:
- A
TOOL_VENDOR_TIMEOUTreaching you means one attempt timed out — not that three did. The side effect may still have committed. - If you retry, yours is the second attempt, not the fifth. Budget accordingly, and reconcile first.
Supplying an idempotencyKey does not change this. The platform forwards it to
the extension as an ordinary input field; whether the vendor deduplicates on it
is the vendor's behaviour, not a platform guarantee, so the platform does not
retry on the strength of one.
Getting the real reason
If you self-host, the untruncated reason is server-side. API Platform logs it
against Governed vendor tool execution failed:
{
"code": "TOOL_VENDOR_EXECUTION_FAILED",
"executionId": "platform-parent-…",
"childExecId": "exec-…",
"executionTier": "microvm",
"planeReason": "…"
}planeReason is the failure text, bounded to 512 characters. Note that
"The operation timed out." there means a client-side budget expired, not that
the platform decided anything — usually the sandbox could not be provisioned in
time.
On Scrydon Cloud, quote the executionId to support.