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.
# `situation` declares which situation this node saves under. It defaults
# to "save_node_output", but pass it explicitly so the choice is visible.
self._output_video_file = ProjectFileParameter(
node=self,
name="output_video_file",
default_filename="output_video.mp4",
situation="save_node_output",
)
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
How the situation is chosen:
The situation argument is the only place a node declares its situation. It is a constructor argument, not a parameter on the node: users never see or edit a situation field on the node face. It defaults to save_node_output, so omitting it is valid, but pass it explicitly so readers of your node can tell which situation is in play.
The UI parameter this creates (conventionally named output_file) holds a filename, not a path. build_file() splits that filename into file_name_base and file_extension, then resolves it through the situation's macro, so the parameter's value is only one part of the final path. A user typing render.png on a save_node_output node gets outputs/MyNode_render.png.
The situation name is not displayed on the parameter's value, but it does appear in the tooltip that add_parameter() generates (Output filename (uses 'save_node_output' situation template)), which is how a user discovers it.
Users override the situation per-node via the cog button on the parameter, which spawns a FileOutputSettings node pre-filled with your declared situation. When that node is connected, build_file() returns its FileDestination and your situation argument is bypassed. Choose the situation that honestly describes your node's operation and let users redirect from there.
❌ 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) in two ways, and your node needs no code changes to respect either:
- Project-wide: redefining a situation in
griptape-nodes-project.ymlchanges it for every node using it. See the Customization Guide. - Per-node: the cog button on your
ProjectFileParameterconnects aFileOutputSettingsnode that overrides the situation, macro, and collision policy for that node alone.
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",
situation="save_node_output",
)
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()