Repository: crewAIInc/crewAI (python)

Issue:
[BUG] Parameter `create_directory` at `Task` class does't exists.
### Description

Parameter `create_directory` at `Task` class doesn't exist.

### Steps to Reproduce

[Parameter `create_directory` at `Task` class does't exists.](https://docs.crewai.com/en/concepts/tasks#creating-directories-when-saving-files)

Reproduce code above.

### Expected behavior

Task would run and directories would be created. 

### Screenshots/Code snippets

```
# ...

save_output_task = Task(
    description='Save the summarized AI news to a file',
    expected_output='File saved successfully',
    agent=research_agent,
    tools=[file_save_tool],
    output_file='outputs/ai_news_summary.txt',
    create_directory=True # <<<<<<<<<<<<<<<< Inexistent argument
)

#...
```

### Operating System

Windows 11

### Python Version

3.12

### crewAI Version

0.130.0

### crewAI Tools Version

0.118.0

### Virtual Environment

Poetry

### Evidence

<img width="564" height="74" alt="Image" src="https://github.com/user-attachments/assets/fd9d7f1f-629f-46c2-8f76-efd2802758bd" />

### Possible Solution

Just implement the parameter or remove it from the documentation.

### Additional context

No

Pull request:
Fix #3149: Add missing create_directory parameter to Task class

# Fix #3149: Add missing create_directory parameter to Task class

## Summary

This PR fixes issue #3149 by implementing the missing `create_directory` parameter in the Task class. Users were encountering `TypeError` when trying to use this parameter that was documented but not implemented.

**Key Changes:**
- Added `create_directory` field to Task class with default value `True` for backward compatibility
- Modified `_save_file` method to respect the `create_directory` parameter
- Added comprehensive test coverage for all scenarios
- Updated class docstring to document the new parameter

**Behavior:**
- When `create_directory=True` (default): Directories are created automatically if they don't exist (maintains existing behavior)
- When `create_directory=False`: Raises `RuntimeError` if the directory doesn't exist, allows file creation if directory exists

## Review & Testing Checklist for Human

- [ ] **End-to-end testing**: Test with actual file paths in your environment to verify directory creation works correctly
- [ ] **Backward compatibility**: Verify that existing code using `output_file` without `create_directory` still works identically
- [ ] **Edge case testing**: Test with absolute paths, relative paths, nested directories, and permission scenarios
- [ ] **Error handling verification**: Confirm that `create_directory=False` properly raises `RuntimeError` when directory doesn't exist

**Recommended Test Plan:**
1. Create a simple Task with `output_file="test/output.txt"` (no `create_directory` param) - should work as before
2. Create a Task with `create_directory=True` and non-existent directory - should create directory and file
3. Create a Task with `create_directory=False` and non-existent directory - should raise RuntimeError
4. Create a Task with `create_directory=False` and existing directory - should create file successfully

---

### Diagram

```mermaid
%%{ init : { "theme" : "default" }}%%
graph TD
    subgraph Legend
        L1["Major Edit"]:::major-edit
        L2["Minor Edit"]:::minor-edit  
        L3["Context/No Edit"]:::context
    end

    A["src/crewai/task.py"]:::major-edit
    B["tests/task_test.py"]:::major-edit
    C["Task.__init__()"]:::major-edit
    D["Task._save_file()"]:::major-edit
    E["GitHub Issue #3149"]:::context
    F["CI Pipeline"]:::context

    E --> A
    A --> C
    A --> D
    C --> B
    D --> B
    B --> F

    classDef major-edit fill:#90EE90
    classDef minor-edit fill:#87CEEB  
    classDef context fill:#FFFFFF
```

### Notes

- **Session Details**: Requested by João (joao@crewai.com) via Slack #joao-test channel
- **Session URL**: https://app.devin.ai/sessions/1c02394d3da24ba9bba178240a3d008e
- **Lint Fix**: Had to remove unused import `os` from test file to pass CI
- **Test Coverage**: Added 5 comprehensive tests covering all scenarios including issue reproduction
- **Path Handling**: The `output_file_validation` method strips leading slashes from absolute paths, converting them to relative paths - this behavior is preserved


Patch:
diff --git a/src/crewai/task.py b/src/crewai/task.py
index 51e1cf00..aa38cfff 100644
--- a/src/crewai/task.py
+++ b/src/crewai/task.py
@@ -67,6 +67,7 @@ class Task(BaseModel):
         description: Descriptive text detailing task's purpose and execution.
         expected_output: Clear definition of expected task outcome.
         output_file: File path for storing task output.
+        create_directory: Whether to create the directory for output_file if it doesn't exist.
         output_json: Pydantic model for structuring JSON output.
         output_pydantic: Pydantic model for task output.
         security_config: Security configuration including fingerprinting.
@@ -115,6 +116,10 @@ class Task(BaseModel):
         description="A file path to be used to create a file output.",
         default=None,
     )
+    create_directory: Optional[bool] = Field(
+        description="Whether to create the directory for output_file if it doesn't exist.",
+        default=True,
+    )
     output: Optional[TaskOutput] = Field(
         description="Task output, it's final result after being executed", default=None
     )
@@ -753,8 +758,10 @@ Follow these guidelines:
             resolved_path = Path(self.output_file).expanduser().resolve()
             directory = resolved_path.parent
 
-            if not directory.exists():
+            if self.create_directory and not directory.exists():
                 directory.mkdir(parents=True, exist_ok=True)
+            elif not self.create_directory and not directory.exists():
+                raise RuntimeError(f"Directory {directory} does not exist and create_directory is False")
 
             with resolved_path.open("w", encoding="utf-8") as file:
                 if isinstance(result, dict):


Test patch:
diff --git a/tests/task_test.py b/tests/task_test.py
index 89fa3c53..40c094ca 100644
--- a/tests/task_test.py
+++ b/tests/task_test.py
@@ -1133,6 +1133,119 @@ def test_output_file_validation():
         )
 
 
+def test_create_directory_true():
+    """Test that directories are created when create_directory=True."""
+    from pathlib import Path
+    
+    output_path = "test_create_dir/output.txt"
+    
+    task = Task(
+        description="Test task",
+        expected_output="Test output",
+        output_file=output_path,
+        create_directory=True,
+    )
+    
+    resolved_path = Path(output_path).expanduser().resolve()
+    resolved_dir = resolved_path.parent
+    
+    if resolved_path.exists():
+        resolved_path.unlink()
+    if resolved_dir.exists():
+        import shutil
+        shutil.rmtree(resolved_dir)
+    
+    assert not resolved_dir.exists()
+    
+    task._save_file("test content")
+    
+    assert resolved_dir.exists()
+    assert resolved_path.exists()
+    
+    if resolved_path.exists():
+        resolved_path.unlink()
+    if resolved_dir.exists():
+        import shutil
+        shutil.rmtree(resolved_dir)
+
+
+def test_create_directory_false():
+    """Test that directories are not created when create_directory=False."""
+    from pathlib import Path
+    
+    output_path = "nonexistent_test_dir/output.txt"
+    
+    task = Task(
+        description="Test task",
+        expected_output="Test output",
+        output_file=output_path,
+        create_directory=False,
+    )
+    
+    resolved_path = Path(output_path).expanduser().resolve()
+    resolved_dir = resolved_path.parent
+    
+    if resolved_dir.exists():
+        import shutil
+        shutil.rmtree(resolved_dir)
+    
+    assert not resolved_dir.exists()
+    
+    with pytest.raises(RuntimeError, match="Directory .* does not exist and create_directory is False"):
+        task._save_file("test content")
+
+
+def test_create_directory_default():
+    """Test that create_directory defaults to True for backward compatibility."""
+    task = Task(
+        description="Test task",
+        expected_output="Test output",
+        output_file="output.txt",
+    )
+    
+    assert task.create_directory is True
+
+
+def test_create_directory_with_existing_directory():
+    """Test that create_directory=False works when directory already exists."""
+    from pathlib import Path
+    
+    output_path = "existing_test_dir/output.txt"
+    
+    resolved_path = Path(output_path).expanduser().resolve()
+    resolved_dir = resolved_path.parent
+    resolved_dir.mkdir(parents=True, exist_ok=True)
+    
+    task = Task(
+        description="Test task",
+        expected_output="Test output",
+        output_file=output_path,
+        create_directory=False,
+    )
+    
+    task._save_file("test content")
+    assert resolved_path.exists()
+    
+    if resolved_path.exists():
+        resolved_path.unlink()
+    if resolved_dir.exists():
+        import shutil
+        shutil.rmtree(resolved_dir)
+
+
+def test_github_issue_3149_reproduction():
+    """Test that reproduces the exact issue from GitHub issue #3149."""
+    task = Task(
+        description="Test task for issue reproduction",
+        expected_output="Test output",
+        output_file="test_output.txt",
+        create_directory=True,
+    )
+    
+    assert task.create_directory is True
+    assert task.output_file == "test_output.txt"
+
+
 @pytest.mark.vcr(filter_headers=["authorization"])
 def test_task_execution_times():
     researcher = Agent(