Skip to content

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:

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:

  • ProjectFileParameter creates a UI parameter that users can configure
  • Call build_file() to get a ProjectFileDestination instance
  • 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 filename parameter 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 sources
  • download_url: When downloading files from URLs
  • save_preview: For generating thumbnail or preview images
  • save_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.yml changes it for every node using it. See the Customization Guide.
  • Per-node: the cog button on your ProjectFileParameter connects a FileOutputSettings node that overrides the situation, macro, and collision policy for that node alone.

Best Practices

  1. Always use the project system for saving files - never use hard-coded paths
  2. Choose the right pattern: Use ProjectFileParameter for user-configurable outputs, ProjectFileDestination for utility functions
  3. Use semantic situations: Pick the situation that best describes your operation
  4. Let macros handle naming: Don't generate UUIDs or timestamps yourself - let the situation's macro and collision policy handle it
  5. Handle temporary files properly: Use Python's tempfile for intermediate processing, only save final results via the project system
  6. 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()