Working with the Project System
The project system is Griptape Nodes' centralized file management framework that handles file organization, naming, and saving across all workflows. It eliminates hard-coded file paths and provides a consistent, configurable approach to file operations. This page covers the node-developer side: how a node saves files through the project system.
Concepts
The concepts behind the project system — the workspace, project templates, situations, macros, directories, and environment variables — are documented in the Project system guides:
- Overview — how the pieces fit together
- Situations — named file-saving scenarios, collision policies, and the full table of default situations
- Macros — the template syntax used to build file paths
- Directories — logical name-to-path mappings referenced in macros
- Customization Guide — how users override paths and situations via
griptape-nodes-project.yml
The short version for node authors: a situation names a file-saving scenario (e.g. save_node_output), its macro template (e.g. {outputs}/{node_name?:_}{file_name_base}{_index?:03}.{file_extension}) generates the concrete path, and users can customize all of it without any changes to your node's code.
Using the Project System in Nodes
There are two main patterns for working with project files in nodes:
Pattern 1: ProjectFileParameter (Recommended for Node Outputs)
Use ProjectFileParameter when your node has a configurable output file parameter that users might want to customize.
from griptape_nodes.exe_types.param_components.project_file_parameter import ProjectFileParameter
from griptape_nodes.exe_types.core_types import Parameter, ParameterMode
from griptape.artifacts.video_url_artifact import VideoUrlArtifact
class MyVideoNode(ControlNode):
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
# Add regular output parameter
self.add_parameter(Parameter(
name="output_video",
output_type="VideoUrlArtifact",
tooltip="Generated video",
allowed_modes={ParameterMode.OUTPUT}
))
# Add project file parameter for output file configuration
self._output_video_file = ProjectFileParameter(
node=self,
name="output_video_file",
default_filename="output_video.mp4",
)
self._output_video_file.add_parameter()
def process(self) -> None:
# ... generate video_bytes ...
# Use build_file() to get a ProjectFileDestination
dest = self._output_video_file.build_file()
saved = dest.write_bytes(video_bytes)
# Set the output parameter with the saved location
self.parameter_output_values["output_video"] = VideoUrlArtifact(saved.location)
Key Points:
ProjectFileParametercreates a UI parameter that users can configure- Call
build_file()to get aProjectFileDestinationinstance - Use
write_bytes()to save the file - Access the saved file's URL/path via
saved.location
❌ Common Mistake: Not Capturing write_bytes() Return Value
# WRONG - Don't do this:
dest = self._output_video_file.build_file()
dest.write_bytes(video_bytes) # ❌ Return value not captured
artifact = VideoUrlArtifact(dest.location) # Using dest, not saved file
# This will fail with: "Failed because missing required variables: file_extension, file_name_base"
Why it fails: Macro variables like {file_extension} and {file_name_base} are resolved when write_bytes() saves the file and returns the saved file object, not by build_file(). Using dest.location before writing causes macro resolution errors.
# CORRECT:
dest = self._output_video_file.build_file()
saved = dest.write_bytes(video_bytes) # ✅ Capture return value
artifact = VideoUrlArtifact(saved.location) # Use saved file's resolved location
The saved object contains the fully resolved file path with all macros filled in.
Pattern 2: ProjectFileDestination Directly (For Utility Functions)
Use ProjectFileDestination.from_situation() directly in utility functions or when you don't need user configuration.
from griptape_nodes.files.project_file import ProjectFileDestination
from griptape.artifacts.video_url_artifact import VideoUrlArtifact
def frames_to_video_artifact(frames: list, fps: int = 30, video_format: str = "mp4") -> VideoUrlArtifact:
"""Convert a list of frames to a VideoUrlArtifact."""
# ... process frames into video_bytes ...
# Save using project file system
dest = ProjectFileDestination.from_situation(
filename=f"video.{video_format}",
situation="save_node_output"
)
saved = dest.write_bytes(video_bytes)
return VideoUrlArtifact(saved.location)
Key Points:
- Use
from_situation()to create a destination with a named situation - The
filenameparameter is the base filename (will be transformed by the situation's macro) - The situation (e.g., "save_node_output") determines the final path and collision behavior
Migration from StaticFilesManager
Old Pattern (Deprecated)
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
import uuid
def old_save_video(video_bytes: bytes) -> VideoUrlArtifact:
filename = f"{uuid.uuid4()}.mp4"
url = GriptapeNodes.StaticFilesManager().save_static_file(video_bytes, filename)
return VideoUrlArtifact(url)
New Pattern
from griptape_nodes.files.project_file import ProjectFileDestination
def new_save_video(video_bytes: bytes) -> VideoUrlArtifact:
dest = ProjectFileDestination.from_situation(
filename="video.mp4",
situation="save_node_output"
)
saved = dest.write_bytes(video_bytes)
return VideoUrlArtifact(saved.location)
Migration Benefits:
- No more UUID generation required
- Consistent file organization across all nodes
- User-configurable file paths via project templates
- Better file tracking and management
- Automatic handling of name collisions
Common Situations and When to Use Them
save_node_output: Primary situation for files generated by nodes (images, videos, audio, etc.)copy_external_file: When importing/copying files from external sourcesdownload_url: When downloading files from URLssave_preview: For generating thumbnail or preview imagessave_static_file: For static assets that don't change between runs
See Situations for the full table of default situations, their macros, and collision policies.
Users can override any of these (paths, macros, collision policies) through their project's griptape-nodes-project.yml — see the Customization Guide. Your nodes automatically respect these customizations without any code changes.
Best Practices
- Always use the project system for saving files - never use hard-coded paths
- Choose the right pattern: Use
ProjectFileParameterfor user-configurable outputs,ProjectFileDestinationfor utility functions - Use semantic situations: Pick the situation that best describes your operation
- Let macros handle naming: Don't generate UUIDs or timestamps yourself - let the situation's macro and collision policy handle it
- Handle temporary files properly: Use Python's
tempfilefor intermediate processing, only save final results via the project system - Clean up temporary files: Always clean up temporary files after copying to the project system
Example: Complete Video Processing Node
import tempfile
from pathlib import Path
from typing import Any
from griptape.artifacts.video_url_artifact import VideoUrlArtifact
from griptape_nodes.exe_types.core_types import Parameter, ParameterMode
from griptape_nodes.exe_types.node_types import ControlNode, AsyncResult
from griptape_nodes.exe_types.param_components.project_file_parameter import ProjectFileParameter
from griptape_nodes.files.file import File
class ProcessVideo(ControlNode):
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self.add_parameter(Parameter(
name="input_video",
input_types=["VideoUrlArtifact"],
type="VideoUrlArtifact",
tooltip="Input video to process"
))
self.add_parameter(Parameter(
name="output_video",
output_type="VideoUrlArtifact",
tooltip="Processed video",
allowed_modes={ParameterMode.OUTPUT}
))
# Add project file parameter for output
self._output_video_file = ProjectFileParameter(
node=self,
name="output_video_file",
default_filename="processed_video.mp4",
)
self._output_video_file.add_parameter()
def process(self) -> AsyncResult:
yield lambda: self._process()
def _process(self) -> None:
# Get input video
input_artifact = self.get_parameter_value("input_video")
input_bytes = File(input_artifact.value).read_bytes()
# Process in temporary location
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as temp_file:
temp_path = Path(temp_file.name)
try:
# Write input to temp file
temp_path.write_bytes(input_bytes)
# ... perform processing on temp_path ...
# Read processed result
output_bytes = temp_path.read_bytes()
# Save using project system
dest = self._output_video_file.build_file()
saved = dest.write_bytes(output_bytes)
# Set output
self.parameter_output_values["output_video"] = VideoUrlArtifact(saved.location)
finally:
# Clean up temporary file
if temp_path.exists():
temp_path.unlink()