twentyhq/twenty · error · Exception

execute_tool failed for {name}

Error message

execute_tool failed for {name}

What it means

Raised by TwentyMCP.call_tool after it routes a catalog tool (anything not in the 4 MCP-native tools) through execute_tool and the JSON-RPC envelope comes back with success: False. The thrown message uses the envelope's error or message field if present, falling back to `execute_tool failed for <name>` only when both are missing. This indicates the inner workspace tool ran but reported a business-logic failure, not a transport error.

Source

Thrown at packages/twenty-server/src/engine/core-modules/tool/tools/code-interpreter-tool/twenty-mcp-helper.const.ts:86

            # companies == {'records': [...], 'count': '5'}
        """
        if not self._available:
            raise RuntimeError('Twenty MCP bridge not available. Missing requests library or credentials.')

        if name in self._MCP_NATIVE_TOOLS:
            return self._raw_mcp_call(name, arguments)

        wrapped = self._raw_mcp_call('execute_tool', {
            'toolName': name,
            'arguments': arguments or {},
        })
        # execute_tool returns one of:
        #   success: { success: True,  message, result: {...} }
        #   failure: { success: False, message, error }
        # Raise on failure, unwrap on success, pass through unknown shapes.
        if isinstance(wrapped, dict):
            if wrapped.get('success') is False:
                raise Exception(wrapped.get('error') or wrapped.get('message') or
                                f"execute_tool failed for {name}")
            if 'result' in wrapped:
                return wrapped['result']
        return wrapped

    def bulk_upsert(self, plural: str, records: list, batch_size: int = 200):
        """
        Upsert many records in batches, paginating to completion.

        This is the recommended write path for imports: upsert dedupes on the
        object's unique fields (e.g. email) server-side, so re-running a partial
        or timed-out import is idempotent. Batches are capped at 200 (the platform
        maximum); the loop runs entirely server-side so the agent never pays the
        per-batch context cost.

        Args:
            plural: Plural object name, e.g. 'companies', 'people'.
            records: List of record dicts to upsert.

View on GitHub (pinned to 1f5dd2bbd2)

Solutions

  1. Read the error/message field embedded in the exception text — it carries the inner tool's failure reason.
  2. For unique-constraint failures, switch to the upsert_many_* tool so the server dedupes on the object's unique fields.
  3. Validate argument shapes against the tool's schema (call `learn_tools` or inspect the catalog) before invoking.
  4. Use bulk_upsert(plural, records) which catches per-batch exceptions and reports them in errors[] rather than aborting the whole import.

Example fix

# before — single call aborts on the first bad record
for person in people:
    twenty.call_tool('create_one_person', person)   # raises execute_tool failed for create_one_person
# after — batched upsert dedupes and isolates per-batch failures
summary = twenty.bulk_upsert('people', people)
if summary['failed']:
    print('partial failure:', summary['errors'])
Defensive patterns

Strategy: retry

Validate before calling

# Pre-validate argument shape against the catalog before calling.
from typing import Any

def validate_args_against_catalog(catalog: dict, tool_name: str, args: dict) -> list[str]:
    schema = catalog.get(tool_name, {}).get('inputSchema', {})
    required = schema.get('required', [])
    errors = []
    for field in required:
        if field not in args:
            errors.append(f'{tool_name}: missing required field {field}')
    return errors

# usage:
catalog = twenty.call_tool('learn_tools', {})  # MCP-native, returns tool catalog
errs = validate_args_against_catalog(catalog, 'create_one_person', person)
if errs:
    raise ValueError(errs[0])

Type guard

from typing import Any

def is_execute_tool_failure(envelope: Any) -> bool:
    return isinstance(envelope, dict) and envelope.get('success') is False

Try / catch

try:
    rec = twenty.call_tool('create_one_company', company)
except Exception as e:
    msg = str(e)
    if 'execute_tool failed' in msg:
        # business-logic failure from the inner tool — do NOT blind-retry; fix args
        if 'unique' in msg.lower() or 'duplicate' in msg.lower():
            rec = twenty.call_tool('upsert_one_company', company)  # switch to upsert to dedupe
        else:
            raise
    else:
        raise

Prevention

When it happens

Trigger: Calling a workspace catalog tool (e.g. create_one_company, upsert_many_people) whose arguments fail server-side validation, violate a unique constraint, reference a non-existent relation id, or hit a permissions error for the token's workspace. The execute_tool envelope wraps that failure as { success: False, error/message: ... } and call_tool re-raises it.

Common situations: LLM-generated calls with wrong field names or types; upserting records that collide on a unique field without the right dedupe key; calling a tool the API token's user lacks permission for; passing a record id from a different workspace.

Related errors


AI-assisted analysis of twentyhq/twenty@1f5dd2bbd2 (2026-08-12). Data as JSON: /api/errors/74b1a15cacc2f4ef. Report an issue: GitHub.