Authoring Libraries
Nodes are distributed as libraries. This page covers the library manifest (griptape_nodes_library.json), declarations, dependency management, documentation conventions, and the process for contributing nodes to the standard library.
Creating Node Libraries
Bundle nodes into libraries for sharing. Create griptape_nodes_library.json:
{
"name": "Library Name",
"library_schema_version": "0.11.0",
"settings": [
{
"description": "API keys required by nodes in this library",
"category": "app_events.on_app_initialization_complete",
"contents": {
"secrets_to_register": ["MY_SERVICE_API_KEY", "MY_OTHER_API_KEY"]
}
}
],
"metadata": {
"author": "Author Name",
"description": "Library description",
"library_version": "1.0.0",
"engine_version": "0.55.0",
"tags": ["AI", "Image Processing"],
"dependencies": {
"pip_dependencies": ["pillow", "requests"],
"pip_install_flags": ["--upgrade"]
},
"declarations": [
{ "type": "lifecycle_stage", "stage": "STABLE" },
{
"type": "model_catalog",
"providers": {
"anthropic": {
"display_name": "Anthropic",
"terms_url": "https://www.anthropic.com/legal/commercial-terms",
"models": {
"claude_opus_byok": {
"display_name": "Claude Opus 4 (BYOK)",
"family": "Claude 4",
"provider_model_id": "claude-opus-4",
"key_support": "REQUIRES_CUSTOMER_KEY"
}
}
}
}
}
]
},
"widgets": [
{
"name": "MyWidget",
"path": "widgets/MyWidget.js",
"description": "Custom UI component for the node"
}
],
"categories": [
{
"image": {
"title": "Image Processing",
"description": "Image manipulation nodes",
"color": "border-purple-500",
"icon": "Image"
}
}
],
"nodes": [
{
"class_name": "MyImageNode",
"file_path": "image/my_image_node.py",
"metadata": {
"category": "image",
"description": "Process images with AI",
"display_name": "AI Image Processor",
"icon": "image",
"group": "processing",
"declarations": [
{ "type": "model_usage", "model_ids": ["claude_opus_byok"] }
]
}
}
],
"workflow_nodes": [
{
"node_type": "UpscaleAndTag",
"workflow_path": "workflows/upscale_and_tag.py",
"metadata": {
"category": "image",
"description": "Upscales an image and tags it",
"display_name": "Upscale and Tag"
}
}
],
"workflows": ["workflows/example_workflow.py"],
"is_default_library": false
}
Library Structure
- settings: Register secrets/API keys used by library nodes
- Use
secrets_to_registerarray to declare required secrets - Category should be
app_events.on_app_initialization_complete - Secrets are accessed via
GriptapeNodes.SecretsManager().get_secret()
- Use
- metadata.dependencies: PIP packages installed on library load
- metadata.declarations / per-node metadata.declarations: typed identity properties (lifecycle stage, arbitrary Python execution) and a library-level model catalog plus per-node references into it. See Library and Node Declarations below.
- widgets: Register custom JS widget components (see Custom Widgets)
- categories: Group nodes in UI with colors and icons
- nodes: List node classes, file paths, and metadata
- advanced_library_path: Optional Python file declaring an
AdvancedNodeLibrarysubclass, for libraries that need to run code at load and unload, own a request type, or register node types the manifest does not list (see Advanced Libraries) - workflow_nodes: Nodes generated from saved workflow files instead of Python classes. See Nodes From Workflow Files below.
- workflows: Template workflow files
Important: The secrets_to_register array tells the system which secrets your library needs. Users will be prompted to configure these secrets through the UI or environment variables.
Use flat directory structures. The engine automatically registers and loads libraries.
Nodes From Workflow Files
A node does not have to be a Python class. Point a workflow_nodes entry at a saved workflow and the
engine generates the node type for you:
"workflow_nodes": [
{
"node_type": "UpscaleAndTag",
"workflow_path": "workflows/upscale_and_tag.py",
"metadata": {
"category": "image",
"description": "Upscales an image and tags it",
"display_name": "Upscale and Tag"
}
}
]
| Field | Meaning |
|---|---|
node_type |
The name the node is registered under. This is what appears in saved workflows. |
workflow_path |
Path to the saved workflow .py, relative to griptape_nodes_library.json. |
metadata |
The same node metadata block the nodes array uses: category, description, display name, etc. |
The workflow needs a Start Flow node and an End Flow node. Those are what define the node's parameters:
- Every non-control parameter on a Start Flow node becomes an input on the node.
- Every non-control parameter on an End Flow node becomes an output on the node.
- The node supplies its own Flow In and Flow Out, so the workflow's control parameters are not surfaced.
- The End Flow node's built-in Status parameters (
was_successful,result_details) are not surfaced either. They report on the End Flow node's own run, not on anything you exposed. A parameter of your own with one of those names is only dropped if you also put it in a parameter group namedStatus.
A parameter name used by exactly one Start Flow or End Flow node keeps its bare name. If two Start
Flow nodes both expose prompt, both parameters are qualified with their node name instead
(Start_Flow.prompt, Start_Flow_2.prompt) so neither is lost. Spaces in the node name become
underscores, because parameter names cannot contain whitespace. A name that appears on both a Start
Flow node and an End Flow node becomes a single parameter that is both an input and an output.
When the node runs, the engine loads the workflow as a child subflow of the node's own flow, copies the node's input values onto the Start Flow nodes, runs it, and copies the End Flow values back out as the node's outputs. The subflow is reused for later runs in the same session and is never written into a saved workflow, so your users' files carry the node, not a copy of the workflow behind it.
Save the workflow from the editor before shipping it. The parameter shape is read out of the metadata header at the top of the workflow file, which the editor writes on save. A workflow with no saved shape (no Start Flow and End Flow nodes, or a hand-written file with no header) is reported as a library problem and its node is not registered.
The workflow is also registered as a workflow in its own right, so it shows up in the editor's
workflow picker. To keep a workflow that only exists to back a node out of that list, set
is_internal = true in its metadata header.
Library and Node Declarations
Declarations attach typed metadata to a library or to an individual node. Each entry in a declarations array is an object with a type discriminator that selects a declaration class. Today's vocabulary covers a lifecycle-stage property, a library-level model catalog with per-node references, and an arbitrary-Python-execution property; future engine releases add more declaration types under the same field.
Both metadata.declarations (library-level) and per-node metadata.declarations accept a list. Order in the list does not matter. The field defaults to [], so libraries on older schema versions (0.6.0, 0.4.0, 0.1.0) load unchanged.
lifecycle_stage
Lifecycle stage for a library or for a specific node. Values:
| Value | Meaning |
|---|---|
STABLE |
Mature; intended for production use. |
BETA |
Feature-complete but still hardening. |
ALPHA |
Early implementation; expect breaking changes. |
LABS |
Exploratory; may be removed. |
DEPRECATED |
Slated for removal; existing usage should migrate to a replacement. |
Semantics:
- Library-level absence is intentionally distinct from
STABLE. A library with nolifecycle_stagedeclaration is "unstated" — consumers should surface that explicitly (<No lifecycle stage provided by library author>) rather than silently assumeSTABLE. - Node-level absence means "inherit the library's stage." A node-level
lifecycle_stageoverrides the library's value.
model_catalog
A library-level declaration of the third-party models nodes in the library can use, organized as a provider → model registry. Identifiers at both levels are dict keys (the key is the stable handle used by node references and admin policies); each entry carries a display_name for UI plus optional terms_url and notes. Each Model additionally declares key_support (required), an optional family grouping tag, and an optional upstream provider_model_id.
The key_support value tells admins what kind of API key authorizes the call:
| Value | Meaning |
|---|---|
REQUIRES_CUSTOMER_KEY |
Customer-supplied API key only. |
SUPPORTS_CUSTOMER_KEY_OR_GRIPTAPE_KEY |
Customer key or Griptape-provided key both work. |
REQUIRES_GRIPTAPE_KEY |
Griptape-provided key only. |
NO_KEY_REQUIRED |
The model runs locally or otherwise needs no API key (e.g. an Ollama-hosted model). |
notes (available on both the provider and the model) is free-form author guidance rendered alongside the entry. Use it for caveats that don't fit other fields, like "BYOK requires injecting a provider-specific prompt driver."
{
"type": "model_catalog",
"providers": {
"anthropic": {
"display_name": "Anthropic",
"terms_url": "https://www.anthropic.com/legal/commercial-terms",
"models": {
"claude_opus_byok": {
"display_name": "Claude Opus 4 (BYOK)",
"family": "Claude 4",
"provider_model_id": "claude-opus-4",
"key_support": "REQUIRES_CUSTOMER_KEY"
},
"claude_opus_griptape": {
"display_name": "Claude Opus 4 (Griptape Key)",
"family": "Claude 4",
"provider_model_id": "claude-opus-4",
"key_support": "REQUIRES_GRIPTAPE_KEY"
}
}
},
"kling": {
"display_name": "Kling",
"terms_url": "https://app.klingai.com/global/about/terms",
"models": {
"kling_v2": {
"display_name": "Kling v2",
"provider_model_id": "kling-v2-master",
"key_support": "REQUIRES_GRIPTAPE_KEY"
}
}
},
"ollama": {
"display_name": "Ollama",
"key_support": "NO_KEY_REQUIRED",
"notes": "Local runtime; models enumerated at runtime, none declared here."
}
}
}
A few rules worth knowing:
familyis just a tag. It clusters related models for display (e.g. the two Claude 4 entries above). It is not a container and is not part of a model's identity, so providers without meaningful families simply omit it.key_supportlives on the model by default. EveryModeldeclares its own value. The same upstream model with two different key requirements becomes two models under two distinct dict keys (seeclaude_opus_byokandclaude_opus_griptapeabove). AModelProvideralso accepts an optionalkey_supportused only when it declares no models at all (e.g. a local-runtime provider like Ollama wherekey_support=NO_KEY_REQUIREDis the only meaningful signal).- Model IDs must be unique across the entire library. Pydantic enforces uniqueness within each provider's
modelsdict for free; collisions across providers are caught at library-load time asDuplicateModelIdProblem. - At most one
model_catalogper library. Declaring two is rejected at validation time; merge their providers into one.
A node's model-selection dropdown stores the provider's own model id (kling-v2-master, not Kling v2), and ModelAccessComponent resolves it to the catalog key that the permission layer gates on. Storing a display name instead means the selection cannot be resolved, so it cannot be checked against license policy. If a dropdown historically stored something else, migrate those saved values with the component's deprecated_values mapping rather than renaming the catalog entry to match them:
self._model_access = ModelAccessComponent(
node=self,
parameter=model_param,
model_choices=["kling-v2-master", "kling-v2-1-master"],
default_model="kling-v2-master",
deprecated_values={"Kling v2": "kling-v2-master", "Kling v2.1": "kling-v2-1-master"},
)
Each key is accepted wherever a value is assigned, including workflow load, and migrated to its mapped choice. Keys are never offered in the dropdown, so retiring a model means moving it from model_choices into deprecated_values pointed at its replacement. Every mapped value must be one of model_choices, and no key may already be a choice; either mistake raises when the node is constructed. If the model you are retiring was also the default_model, point default_model at its replacement in the same edit -- default_model must always name a current choice, never a retired key, and the component raises if it does not.
model_usage
A node references one or more catalog models by their dict keys. Use this when the node binds to a specific, named set of models. Each entry must resolve to a model somewhere in the catalog at library-load time; unresolved references surface as UnresolvedModelUsageReferenceProblem.
{ "type": "model_usage", "model_ids": ["claude_opus_byok", "kling_v2"] }
model_provider_usage
A node references one or more entire providers. Use this when a node dynamically enumerates every model a provider offers at runtime. Each entry must resolve to a provider declared in the catalog; unresolved references surface as UnresolvedModelProviderUsageReferenceProblem.
{ "type": "model_provider_usage", "provider_ids": ["anthropic", "ollama"] }
The two usage declarations are independent. A node can carry any combination — for instance, "every model this provider offers, plus these two specific models from another provider."
arbitrary_python_execution
Declares that a node executes arbitrary Python code supplied at runtime (for example, an artist-authored script). Node-level only. This is a security-relevant identity fact: consumers (UI) can warn an artist before the node runs. Absence of this declaration means the node does not execute arbitrary Python.
| Field | Meaning |
|---|---|
executes_arbitrary_python |
true when the node runs unvetted, runtime-supplied Python. |
"declarations": [
{ "type": "arbitrary_python_execution", "executes_arbitrary_python": true }
]
Combining declarations
A node can carry any combination of declarations. For example, a Labs node that uses two models:
"metadata": {
"category": "labs",
"description": "Labs node demonstrating multiple declarations.",
"display_name": "Labs Node",
"declarations": [
{ "type": "lifecycle_stage", "stage": "LABS" },
{ "type": "model_usage", "model_ids": ["claude_opus_byok", "kling_v2"] }
]
}
New declaration types added in future engine releases land additively under this same declarations field without a schema-version bump.
Library Structure with uv Dependency Management
Modern Approach: Use uv for fast, reproducible dependency management following the Minimax library pattern.
Directory Structure
library-name/
├── pyproject.toml # uv configuration
├── uv.lock # Lock file (generated)
├── LICENSE # License file
├── README.md # Documentation
├── CHANGELOG.md # Version history
├── .gitignore # Ignore rules
└── library_name/
├── griptape_nodes_library.json # Library metadata
└── node_file.py
pyproject.toml Configuration
[project]
name = "library-name"
version = "1.0.0"
description = "Description of your library"
authors = [
{name = "Your Name", email = "email@example.com"}
]
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"requests",
# Add other packages your nodes import at runtime
]
[dependency-groups]
dev = ["griptape-nodes-engine", "pytest", "pyright", "ruff"]
[tool.uv.sources]
griptape-nodes-engine = { git = "https://github.com/griptape-ai/griptape-nodes-engine", rev = "latest" }
[tool.hatch.build.targets.wheel]
packages = ["library_name"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
Do not list the engine as a runtime dependency
The engine is the host that loads your library, not a package your library pulls in. Keep griptape-nodes-engine out of [project] dependencies:
- Installing your library would otherwise install a second engine. The engine puts a library's virtual environment at the front of its own import path, so that second copy can shadow the engine that is actually running, and the resulting errors look like engine bugs rather than library ones.
[dependency-groups] devstill gives your tests, type checker, and editor an engine to resolve against, becauseuv syncinstalls dev groups by default. Your development workflow is unchanged.- The engine version your library needs belongs in
engine_versionin your library JSON. That is the value the engine actually checks when it loads you; the pyproject specifier is never consulted at load time.
Declaring it in both places means maintaining the same fact twice, and the two drift.
This changes with library packaging
Once libraries are resolved as packages into their own environments, the engine becomes a normal bounded dependency (griptape-nodes-engine>=X,<Y) and that single resolution replaces the engine_version check. Use the layout above until that ships.
Library Configuration (inside subdirectory)
Place griptape_nodes_library.json inside the library subdirectory:
{
"name": "Library Name",
"library_schema_version": "0.1.0",
"settings": [
{
"description": "API keys required by nodes",
"category": "app_events.on_app_initialization_complete",
"contents": {
"secrets_to_register": ["API_KEY_NAME"]
}
}
],
"nodes": [
{
"class_name": "NodeClassName",
"file_path": "node_file.py", // Relative to library subdirectory
"metadata": {
"category": "category_name",
"description": "Node description",
"display_name": "Node Display Name"
}
}
]
}
Installation Instructions in README
Provide both uv (recommended) and pip (fallback) installation methods:
## Installation
### Option 1: Using uv (Recommended)
1. Clone or download this library
2. Install dependencies:
```bash
cd library-name
uv sync
```
3. Place in Griptape Nodes libraries directory
### Option 2: Automatic Installation
1. Place folder in libraries directory
2. Dependencies install automatically via pip
Generate Lock File
cd library-name
uv sync
Benefits:
- Fast installation (Rust-based)
- Reproducible builds via lock file
- Direct GitHub integration for griptape-nodes
- Backward compatible with pip installation
Documentation Patterns for Node Libraries
Comprehensive README Structure
# Library Name
Brief description and key features.
## Features
- Bullet list of main capabilities
- Include model options
- Highlight unique features
## Installation
### Option 1: Using uv (Recommended)
Steps for uv installation
### Option 2: Automatic Installation
Steps for pip installation
## Getting Started
### Simple Mode (Recommended for First-Time Users)
Minimal example with explanations
### Custom Mode (Advanced)
Advanced example showing all features
## Parameters
### Basic Parameters
Table with Name, Type, Description
### Advanced Parameters (Hidden by Default)
Table with Name, Type, Default, Description
### Output Parameters
Table with outputs
## Model Comparison
Table comparing models:
| Model | Max Duration | Quality | Speed | Character Limits |
## Character Limits
Clear tables showing limits by model/mode
## API Rate Limits
Document:
- Concurrency limits
- Generation time expectations
- File retention policies
## Example Workflows
3-5 complete examples covering common use cases
## Error Handling
Common errors and solutions
## Troubleshooting
FAQ-style troubleshooting guide
## API Reference
Link to official API docs
## Best Practices
Tips for optimal usage
## Support
Where to get help
## Version History
Link to CHANGELOG
Troubleshooting Entries Worth Documenting
Two errors come up often enough that a library README's troubleshooting section should cover them:
Error: "Missing required variables: file_extension, file_name_base"
Full Error:
ERROR: Attempted to resolve macro path. Failed because missing required variables: file_extension, file_name_base
ERROR: Attempted to create download URL. Failed with file_path='{outputs}/{node_name?:_}{file_name_base}{_index?:03}.{file_extension}'
Cause: Not capturing the return value from write_bytes(). Using dest.location instead of saved.location.
Incorrect Code:
dest = self._output_file.build_file()
dest.write_bytes(video_bytes) # ❌ Return value not captured
artifact = VideoUrlArtifact(dest.location) # Using dest, not saved file
Solution:
dest = self._output_file.build_file()
saved = dest.write_bytes(video_bytes) # ✅ Capture the saved file
artifact = VideoUrlArtifact(saved.location) # Use saved file's resolved location
Explanation: Macro variables are populated when write_bytes() actually saves the file. The saved object returned by write_bytes() contains the fully resolved path.
Error: Type Conversion Issues with Image Parameters
Symptom: Image parameters don't handle different input types consistently, or errors occur when passing URLs, file paths, or artifacts between nodes.
Problem: Using generic Parameter with manual type configuration doesn't standardize type conversion logic:
# ❌ Inconsistent type handling
Parameter(
name="image",
input_types=["ImageArtifact", "ImageUrlArtifact", "str"],
type="ImageArtifact",
)
Solution: Use ParameterImage for standardized type conversion:
# ✅ Standardized type handling
ParameterImage(
name="image",
tooltip="Input image",
allow_output=False,
)
Benefits:
- Declares
ImageUrlArtifactinstead ofImageArtifact, keeping the parameter's value small — see Parameter Payload Size - Handles ImageArtifact, ImageUrlArtifact, and strings consistently
- Built-in support for URLs, file paths, and data URIs
- Graceful error handling for various input formats
- Reduces type conversion errors in complex workflows
Model Comparison Table
Always include a comparison table for services with multiple models:
| Model | Max Duration | Quality | Speed | Character Limits |
| ----- | ------------ | -------- | ------- | ------------------------- |
| V5 | 4 min | Superior | Fastest | Prompt: 5000, Style: 1000 |
| V4_5 | 8 min | High | Fast | Prompt: 5000, Style: 1000 |
| V4 | 4 min | Best | Medium | Prompt: 3000, Style: 200 |
Contributing to the Standard Library
When adding nodes to the core griptape_nodes_library (as opposed to creating a standalone library), follow this process:
1. Create a Feature Branch
cd griptape-nodes
git checkout -b feature/add-color-match-node
2. Add the Node File
Place your node in the appropriate category subdirectory:
libraries/griptape_nodes_library/griptape_nodes_library/
├── image/
│ ├── color_match.py # New node file
│ ├── load_image.py
│ └── save_image.py
├── text/
├── audio/
└── ...
3. Update griptape_nodes_library.json
Make three updates to libraries/griptape_nodes_library/griptape_nodes_library.json:
a. Increment the library version
{
"metadata": {
"library_version": "0.59.0" // Was 0.58.0
}
}
b. Add any new pip dependencies
{
"metadata": {
"dependencies": {
"pip_dependencies": [
"existing-dep",
"color-matcher" // New dependency
]
}
}
}
c. Add the node entry
{
"nodes": [
{
"class_name": "ColorMatch",
"file_path": "griptape_nodes_library/image/color_match.py",
"metadata": {
"category": "image",
"description": "Transfer color characteristics from a reference image to a target image",
"display_name": "Color Match",
"icon": "palette",
"group": "edit"
}
}
]
}
4. Add Documentation
Create a documentation page at docs/nodes/<category>/<node_name>.md:
# Color Match
Transfer color characteristics from a reference image to a target image.
## What It Does
Applies the color palette from a reference image to a target image...
## Parameters
### Inputs
| Parameter | Type | Description |
| --------------- | ---------------- | --------------------------- |
| reference_image | ImageUrlArtifact | Source of the color palette |
| target_image | ImageUrlArtifact | Image to apply colors to |
### Outputs
| Parameter | Type | Description |
| ------------ | ---------------- | -------------------- |
| output_image | ImageUrlArtifact | Color-matched result |
## Example Usage
1. Connect a reference image with desired colors
2. Connect the target image to transform
3. Run the node
## Technical Details
Uses the color-matcher library with histogram matching...
5. Update mkdocs.yml Navigation
Add your doc page to the navigation in mkdocs.yml:
nav:
- Nodes Reference:
- Image:
- Load Image: nodes/image/load_image.md
- Save Image: nodes/image/save_image.md
- Color Match: nodes/image/color_match.md # New entry
6. Run Quality Checks
Before committing, run formatting and checks:
make format # Auto-format code
make check/lint # Check for linting issues
make check/types # Check for type errors
Fix any issues that arise before proceeding.
7. Commit and Create PR
git add .
git commit -m "feat(image): add ColorMatch node for color transfer"
git push -u origin HEAD
gh pr create --title "Add ColorMatch node" --body "## Summary
- Adds ColorMatch node for transferring colors between images
- Uses color-matcher library
- Includes documentation
## Test plan
- [ ] Load two images
- [ ] Run color match
- [ ] Verify output has reference colors"
Standard Library vs External Library
| Aspect | Standard Library | External Library |
|---|---|---|
| Location | griptape-nodes repo |
Separate repo |
| Installation | Included by default | User installs |
| Review | Requires PR approval | Self-published |
| Dependencies | Added to core griptape_nodes_library.json |
Own griptape_nodes_library.json |
| Versioning | Follows core library version | Independent versioning |
| Docs | Added to main docs site | README in library |
When to contribute to standard library:
- Node has broad utility for many users
- No proprietary/paid API dependencies
- Stable, well-tested implementation
- Follows all code quality standards
When to create external library:
- Niche use case
- Requires paid API keys
- Experimental/rapidly changing
- Want independent release cycle