fix(tools): fall back to CREWAI_API_URL/CREWAI_BEARER_TOKEN env vars

InvokeCrewAIAutomationTool required crew_api_url and crew_bearer_token
as positional __init__ arguments with no environment-variable fallback,
even though docs.crewai.com documents CREWAI_API_URL/CREWAI_BEARER_TOKEN
as alternatives. It also could not be instantiated with zero arguments,
which blocks CrewAI AMP Studio's "Invoke Amp Automation" internal tool:
the Studio runtime resolves tools by class reference and instantiates
them with no arguments.

- crew_api_url/crew_bearer_token become optional, falling back to
  CREWAI_API_URL/CREWAI_BEARER_TOKEN (explicit args still win), mirroring
  the env-var pattern already used by GenerateCrewaiAutomationTool.
- crew_name/crew_description become optional too, defaulting to the
  tool's existing generic name/description, so
  InvokeCrewAIAutomationTool() never raises at construction time.
- Declare env_vars: list[EnvVar] so the tool catalog surfaces the two
  env vars, matching the sibling tool.
- Raise a clear ValueError at use time when crew_api_url/crew_bearer_token
  are still missing, instead of failing inside `requests` or with the
  previous confusing TypeError about positional arguments.
- Update the tool's README and the edge docs page
  (docs/edge/en/tools/integration/crewaiautomationtool.mdx) so the "Tool
  Arguments" table matches the corrected code.

Fully backward compatible: existing positional/keyword constructor calls
are unchanged. tool.specs.json is left untouched; the
"Generate Tool Specifications" CI workflow regenerates and commits it
automatically once this is pushed.

crewAIInc/crewAI-tools (the previous home of this tool) is archived and
can no longer receive pushes, so this fix targets the actively
maintained copy of the tool under lib/crewai-tools/ instead.
This commit is contained in:
Mateus Braga
2026-09-11 13:54:05 +00:00
parent e1f3c4bdd4
commit eb281c6d73
4 changed files with 328 additions and 28 deletions

View File

@@ -68,13 +68,17 @@ print(result)
| Argument | Type | Required | Default | Description |
|:---------|:-----|:---------|:--------|:------------|
| **crew_api_url** | `str` | Yes | None | Base URL of the CrewAI Platform automation API |
| **crew_bearer_token** | `str` | Yes | None | Bearer token for API authentication |
| **crew_name** | `str` | Yes | None | Name of the crew automation |
| **crew_description** | `str` | Yes | None | Description of what the crew automation does |
| **crew_api_url** | `str` | No | `None` | Base URL of the CrewAI Platform automation API. Falls back to the `CREWAI_API_URL` environment variable when omitted. |
| **crew_bearer_token** | `str` | No | `None` | Bearer token for API authentication. Falls back to the `CREWAI_BEARER_TOKEN` environment variable when omitted. |
| **crew_name** | `str` | No | generic name | Name of the crew automation. Set explicitly outside CrewAI AMP Studio so the LLM sees a meaningful tool name. |
| **crew_description** | `str` | No | generic description | Description of what the crew automation does. Set explicitly for the same reason as `crew_name`. |
| **max_polling_time** | `int` | No | 600 | Maximum time in seconds to wait for task completion |
| **crew_inputs** | `dict` | No | None | Dictionary defining custom input schema fields |
If `crew_api_url`/`crew_bearer_token` are still missing at run time (neither passed
explicitly nor available via the environment variables below), the tool raises a clear
error explaining what's missing.
## Environment Variables
```bash

View File

@@ -39,6 +39,28 @@ tool = InvokeCrewAIAutomationTool(
result = tool.run()
```
### Using Environment Variables
`crew_api_url` and `crew_bearer_token` can also be provided via environment variables
instead of constructor arguments, which is convenient for keeping credentials out of code:
```shell
export CREWAI_API_URL="https://data-analysis-crew-[...].crewai.com"
export CREWAI_BEARER_TOKEN="your_bearer_token_here"
```
```python
from crewai_tools import InvokeCrewAIAutomationTool
# crew_api_url/crew_bearer_token are read from CREWAI_API_URL/CREWAI_BEARER_TOKEN
tool = InvokeCrewAIAutomationTool(
crew_name="Data Analysis Crew",
crew_description="Analyzes data and generates insights"
)
```
Explicit constructor arguments always take precedence over the environment variables.
### Advanced Usage with Custom Inputs
```python
@@ -109,18 +131,29 @@ result = crew.kickoff()
## Arguments
### Required Parameters
- `crew_api_url` (str): Base URL of the CrewAI Platform automation API
- `crew_bearer_token` (str): Bearer token for API authentication
- `crew_name` (str): Name of the crew automation
- `crew_description` (str): Description of what the crew automation does
### Optional Parameters
- `crew_api_url` (str): Base URL of the CrewAI Platform automation API. Falls back to the
`CREWAI_API_URL` environment variable when omitted.
- `crew_bearer_token` (str): Bearer token for API authentication. Falls back to the
`CREWAI_BEARER_TOKEN` environment variable when omitted.
- `crew_name` (str): Name of the crew automation. Defaults to a generic tool name; set it
explicitly so the LLM sees a meaningful tool name and description in its tool list.
- `crew_description` (str): Description of what the crew automation does. Defaults to a
generic description for the same reason as `crew_name`.
- `max_polling_time` (int): Maximum time in seconds to wait for task completion (default: 600 seconds = 10 minutes)
- `crew_inputs` (dict): Dictionary defining custom input schema fields using Pydantic Field objects
All of the above are optional at construction time, so `InvokeCrewAIAutomationTool()` never
fails to instantiate. If `crew_api_url`/`crew_bearer_token` are still missing (neither passed
explicitly nor available via `CREWAI_API_URL`/`CREWAI_BEARER_TOKEN`) when the tool is actually
run, it raises a clear `ValueError` explaining what is missing.
## Environment Variables
- `CREWAI_API_URL`: Alternative to passing `crew_api_url`.
- `CREWAI_BEARER_TOKEN`: Alternative to passing `crew_bearer_token`.
## Custom Input Schema
When defining `crew_inputs`, use Pydantic Field objects to specify the input parameters. These have to be compatible with the crew automation you are invoking:

View File

@@ -1,11 +1,20 @@
import os
import time
from typing import Any
from crewai.tools import BaseTool
from crewai.tools import BaseTool, EnvVar
from pydantic import BaseModel, Field, create_model
import requests
# Generic fallbacks used when the tool is instantiated without a crew_name /
# crew_description, e.g. by a runtime that resolves tools by class reference
# and instantiates them with no arguments (see the "Zero-argument
# instantiation" note in the class docstring below).
DEFAULT_TOOL_NAME = "invoke_amp_automation"
DEFAULT_TOOL_DESCRIPTION = "Invokes an CrewAI Platform Automation using API"
class InvokeCrewAIAutomationInput(BaseModel):
"""Input schema for InvokeCrewAIAutomationTool."""
@@ -59,32 +68,72 @@ class InvokeCrewAIAutomationTool(BaseTool):
... },
... )
... ]
Configuring the API url and bearer token via environment variables, instead of
passing them as constructor arguments:
>>> import os
>>> os.environ["CREWAI_API_URL"] = "https://canary-crew-[...].crewai.com"
>>> os.environ["CREWAI_BEARER_TOKEN"] = "[Your token: abcdef012345]"
>>> tool = InvokeCrewAIAutomationTool(
... crew_name="State of AI Report",
... crew_description="Retrieves a report on state of AI for a given year.",
... )
Zero-argument instantiation:
`crew_api_url`/`crew_bearer_token` fall back to the `CREWAI_API_URL` /
`CREWAI_BEARER_TOKEN` environment variables, and `crew_name` /
`crew_description` fall back to a generic name/description, so
`InvokeCrewAIAutomationTool()` never raises at construction time. This
matters because some runtimes (e.g. the CrewAI AMP Studio "Invoke Amp
Automation" internal tool) resolve tools by class reference and instantiate
them with no arguments. If the tool is still unconfigured (no explicit
arguments and no environment variables) when it is actually invoked, it
raises a clear `ValueError` instead of attempting the HTTP request.
"""
name: str = "invoke_amp_automation"
description: str = "Invokes an CrewAI Platform Automation using API"
name: str = DEFAULT_TOOL_NAME
description: str = DEFAULT_TOOL_DESCRIPTION
args_schema: type[BaseModel] = InvokeCrewAIAutomationInput
crew_api_url: str
crew_bearer_token: str
crew_api_url: str | None = None
crew_bearer_token: str | None = None
max_polling_time: int = 10 * 60
env_vars: list[EnvVar] = Field(
default_factory=lambda: [
EnvVar(
name="CREWAI_API_URL",
description="Base URL of the crew/flow API to invoke. Alternative to passing crew_api_url.",
required=True,
),
EnvVar(
name="CREWAI_BEARER_TOKEN",
description="Bearer token used to authenticate against crew_api_url. Alternative to passing crew_bearer_token.",
required=True,
),
]
)
def __init__(
self,
crew_api_url: str,
crew_bearer_token: str,
crew_name: str,
crew_description: str,
crew_api_url: str | None = None,
crew_bearer_token: str | None = None,
crew_name: str = DEFAULT_TOOL_NAME,
crew_description: str = DEFAULT_TOOL_DESCRIPTION,
max_polling_time: int = 10 * 60,
crew_inputs: dict[str, Any] | None = None,
):
"""Initialize the InvokeCrewAIAutomationTool.
Args:
crew_api_url: Base URL of the crew API service
crew_bearer_token: Bearer token for API authentication
crew_name: Name of the crew to invoke
crew_description: Description of the crew to invoke
crew_api_url: Base URL of the crew API service. If omitted, falls back to
the CREWAI_API_URL environment variable.
crew_bearer_token: Bearer token for API authentication. If omitted, falls
back to the CREWAI_BEARER_TOKEN environment variable.
crew_name: Name of the crew to invoke. Defaults to a generic tool name so
the tool can be instantiated without arguments; set it explicitly so
the LLM sees a meaningful tool name in its tool list.
crew_description: Description of the crew to invoke. Defaults to a generic
description for the same reason as crew_name.
max_polling_time: Maximum time in seconds to wait for task completion (default: 600 seconds = 10 minutes)
crew_inputs: Optional dictionary defining custom input schema fields
"""
@@ -102,15 +151,45 @@ class InvokeCrewAIAutomationTool(BaseTool):
else:
args_schema = InvokeCrewAIAutomationInput
# Explicit constructor arguments win over the environment variables.
resolved_api_url = crew_api_url or os.getenv("CREWAI_API_URL")
resolved_bearer_token = crew_bearer_token or os.getenv("CREWAI_BEARER_TOKEN")
super().__init__(
name=crew_name,
description=crew_description,
name=crew_name or DEFAULT_TOOL_NAME,
description=crew_description or DEFAULT_TOOL_DESCRIPTION,
args_schema=args_schema,
crew_api_url=crew_api_url,
crew_bearer_token=crew_bearer_token,
crew_api_url=resolved_api_url,
crew_bearer_token=resolved_bearer_token,
max_polling_time=max_polling_time,
)
def _ensure_configured(self) -> None:
"""Raise a clear, actionable error if the tool has no API url/token.
crew_api_url and crew_bearer_token are optional at construction time (they
may be resolved later from the environment, or simply not set yet when the
tool is instantiated with no arguments). This is checked once, right before
the tool is actually used, so a missing configuration produces an explicit
error message instead of a confusing failure deep inside `requests` (e.g. an
"Invalid URL 'None/kickoff'" error) or a bare 401 from the API.
"""
missing = [
env_name
for value, env_name in (
(self.crew_api_url, "CREWAI_API_URL"),
(self.crew_bearer_token, "CREWAI_BEARER_TOKEN"),
)
if not value
]
if missing:
raise ValueError(
"InvokeCrewAIAutomationTool is not configured: missing "
f"{' and '.join(missing)}. Pass crew_api_url/crew_bearer_token "
"explicitly when creating the tool, or set the "
f"{' and '.join(missing)} environment variable(s)."
)
def _kickoff_crew(self, inputs: dict[str, Any]) -> dict[str, Any]:
"""Start a new crew task.
@@ -154,6 +233,8 @@ class InvokeCrewAIAutomationTool(BaseTool):
def _run(self, **kwargs: Any) -> str:
"""Execute the crew invocation tool."""
self._ensure_configured()
if kwargs is None:
kwargs = {}

View File

@@ -0,0 +1,182 @@
import os
from unittest.mock import MagicMock, patch
from crewai_tools.tools.invoke_crewai_automation_tool.invoke_crewai_automation_tool import (
DEFAULT_TOOL_DESCRIPTION,
DEFAULT_TOOL_NAME,
InvokeCrewAIAutomationTool,
)
from pydantic import Field
import pytest
@pytest.fixture(autouse=True)
def clean_env():
"""Ensure CREWAI_API_URL/CREWAI_BEARER_TOKEN never leak between tests."""
with patch.dict(os.environ, {}, clear=False):
os.environ.pop("CREWAI_API_URL", None)
os.environ.pop("CREWAI_BEARER_TOKEN", None)
yield
def test_zero_argument_instantiation_does_not_raise():
"""A runtime that resolves tools by class reference (e.g. CrewAI AMP Studio's
"Invoke Amp Automation" internal tool) instantiates the tool with no arguments.
This must never raise a TypeError about missing positional arguments."""
tool = InvokeCrewAIAutomationTool()
assert tool.name == DEFAULT_TOOL_NAME
assert DEFAULT_TOOL_DESCRIPTION in tool.description
assert tool.crew_api_url is None
assert tool.crew_bearer_token is None
def test_existing_positional_keyword_call_shape_still_works():
"""Backward compatibility: the documented/previous required-arguments call shape
keeps working unchanged."""
tool = InvokeCrewAIAutomationTool(
"https://api.example.com",
"explicit_token",
"My Crew",
"Description of what the crew does",
)
assert tool.crew_api_url == "https://api.example.com"
assert tool.crew_bearer_token == "explicit_token"
assert tool.name == "My Crew"
assert "Description of what the crew does" in tool.description
kwarg_tool = InvokeCrewAIAutomationTool(
crew_api_url="https://api.example.com",
crew_bearer_token="explicit_token",
crew_name="My Crew",
crew_description="Description of what the crew does",
max_polling_time=120,
)
assert kwarg_tool.crew_api_url == "https://api.example.com"
assert kwarg_tool.crew_bearer_token == "explicit_token"
assert kwarg_tool.max_polling_time == 120
def test_env_var_fallback_for_url_and_token():
with patch.dict(
os.environ,
{
"CREWAI_API_URL": "https://from-env.crewai.com",
"CREWAI_BEARER_TOKEN": "env_token",
},
):
tool = InvokeCrewAIAutomationTool(
crew_name="My Crew", crew_description="Does things"
)
assert tool.crew_api_url == "https://from-env.crewai.com"
assert tool.crew_bearer_token == "env_token"
assert tool.name == "My Crew"
assert "Does things" in tool.description
def test_explicit_arguments_take_precedence_over_env_vars():
with patch.dict(
os.environ,
{
"CREWAI_API_URL": "https://from-env.crewai.com",
"CREWAI_BEARER_TOKEN": "env_token",
},
):
tool = InvokeCrewAIAutomationTool(
crew_api_url="https://explicit.crewai.com",
crew_bearer_token="explicit_token",
crew_name="My Crew",
crew_description="Does things",
)
assert tool.crew_api_url == "https://explicit.crewai.com"
assert tool.crew_bearer_token == "explicit_token"
def test_missing_configuration_raises_clear_error_on_use():
"""No explicit args and no env vars: construction succeeds, but running the
tool must raise a clear, actionable error instead of failing deep inside
`requests` or the CrewAI Platform API with a bare 401."""
tool = InvokeCrewAIAutomationTool(
crew_name="My Crew", crew_description="Does things"
)
with pytest.raises(ValueError) as exc_info:
tool.run(prompt="hello")
message = str(exc_info.value)
assert "CREWAI_API_URL" in message
assert "CREWAI_BEARER_TOKEN" in message
def test_partial_configuration_raises_clear_error_on_use():
"""Only one of the two required values is configured."""
tool = InvokeCrewAIAutomationTool(
crew_api_url="https://api.example.com",
crew_name="My Crew",
crew_description="Does things",
)
with pytest.raises(ValueError) as exc_info:
tool.run(prompt="hello")
message = str(exc_info.value)
assert "CREWAI_BEARER_TOKEN" in message
assert "CREWAI_API_URL" not in message
@patch("requests.get")
@patch("requests.post")
def test_successful_run_with_env_var_configuration(mock_post, mock_get):
with patch.dict(
os.environ,
{
"CREWAI_API_URL": "https://from-env.crewai.com",
"CREWAI_BEARER_TOKEN": "env_token",
},
):
tool = InvokeCrewAIAutomationTool(
crew_name="My Crew", crew_description="Does things"
)
kickoff_response = MagicMock()
kickoff_response.json.return_value = {"kickoff_id": "kickoff-123"}
mock_post.return_value = kickoff_response
status_response = MagicMock()
status_response.json.return_value = {"state": "success", "result": "42"}
mock_get.return_value = status_response
result = tool.run(prompt="hello")
assert result == "42"
mock_post.assert_called_once_with(
"https://from-env.crewai.com/kickoff",
headers={
"Authorization": "Bearer env_token",
"Content-Type": "application/json",
},
json={"inputs": {"prompt": "hello"}},
timeout=30,
)
def test_dynamic_crew_inputs_schema_still_works():
custom_inputs = {
"year": Field(..., description="Year to retrieve the report for (integer)"),
"region": Field(default="global", description="Geographic region"),
}
tool = InvokeCrewAIAutomationTool(
crew_api_url="https://api.example.com",
crew_bearer_token="token",
crew_name="State of AI Report",
crew_description="Retrieves a report on state of AI for a given year.",
crew_inputs=custom_inputs,
)
schema_fields = tool.args_schema.model_fields
assert "year" in schema_fields
assert "region" in schema_fields