Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/skills/node-create/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ func New<Provider><Action>Node(nodeID string) *<Provider><Action>Node {
return &<Provider><Action>Node{
Node: nodes.Node{
ID: nodeID,
Kind: nodes.NodeKindAction,
Version: "0.0.1",
Name: "<DisplayName>",
Description: "<one-line description>",
Expand Down
12 changes: 8 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ MCP_API_READ_TIMEOUT=120s
MCP_API_WRITE_TIMEOUT=120s
MCP_API_IDLE_TIMEOUT=180s
MCP_API_REDUCE_MEMORY_USAGE=false
# Wildcard on purpose: MCP clients are not browsers and have no fixed origin.
# Every MCP route is behind an API key, so the browser same-origin policy is
# not the control here. Every other service pins its origins.
MCP_API_ALLOW_ORIGINS=*
MCP_API_ALLOW_HEADERS='Origin, Content-Type, Accept, Authorization, X-API-Key, Mcp-Session-Id, Mcp-Protocol-Version'
MCP_API_ALLOW_METHODS=GET,POST,DELETE
Expand Down Expand Up @@ -346,10 +349,10 @@ FILE_GATEWAY_AUTH_JWT_TOKEN_LEEWAY=0s
# Supported drivers: local | s3 | bunny
FILE_GATEWAY_STORAGE_DRIVER=local

FILE_GATEWAY_STORAGE_LOCAL_PUBLIC_PATH=/tmp/uploads/public
FILE_GATEWAY_STORAGE_LOCAL_PUBLIC_PATH=/data/uploads/public
FILE_GATEWAY_STORAGE_LOCAL_PUBLIC_BASE_URL="${FILE_GATEWAY_API_BASE_URL}/uploads"

FILE_GATEWAY_STORAGE_LOCAL_PRIVATE_PATH=/tmp/uploads/private
FILE_GATEWAY_STORAGE_LOCAL_PRIVATE_PATH=/data/uploads/private

FILE_GATEWAY_STORAGE_S3_PUBLIC_HOST=https://cdn.blocknext.ai
FILE_GATEWAY_STORAGE_S3_PUBLIC_ACCESS_KEY_ID=local-development-access-key
Expand Down Expand Up @@ -380,7 +383,8 @@ FILE_GATEWAY_DOWNLOAD_TIMEOUT=30s
VITE_PLATFORM_API_URL="${PLATFORM_API_BASE_URL}"
VITE_MCP_API_URL="${MCP_API_BASE_URL}"

VITE_WEB3_LOGIN_ENABLED=true

# Docker compose service
# Image tag the stack runs; unset means latest. Pin it before upgrading.
# BLOCKNEXT_VERSION=latest

COMPOSE_PROFILES=cache-${CACHE_TYPE},broker-${BROKER_TYPE},lock-${TASK_RUNNER_LEADER_PROVIDER},semaphore-${TASK_RUNNER_SEMAPHORE_PROVIDER},task-runner-${TASK_RUNNER_MODE}
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ It is built for end users, not just developers: flows stay simple, readable and
## Highlights

- **Visual flow builder** — compose workflows on an intuitive drag-and-drop canvas. Flows are deliberately simple: no loops, no sub-workflows, no expression language to learn.
- **Describe it, don't configure it** — give a node plain-language instructions and an LLM fills in its parameters at run time. Anything you set explicitly always wins over what the model infers.
- **Describe it, don't configure it** — give a node plain-language instructions and an LLM fills in its parameters at run time. Anything you set explicitly always wins over what the model infers. This one and flow generation are the only features that need a key of your own (Gemini); both are off by default. Everything else — the canvas, triggers, the MCP server, every integration node — works on a fresh install.
- **Built-in MCP server** — every integration node doubles as an [MCP](https://modelcontextprotocol.io) tool. Point Claude (or any MCP client) at your BlockNext server with an API key and use your connected services from chat.
- **AI-powered nodes** — LLMs and generative AI (text, image, audio, video) as first-class building blocks, alongside integrations for the tools you already use.
- **Async done right** — long-running AI jobs (video, music generation) are a single node. The node starts the job, polls the provider, and returns the finished result — no wait/check/loop scaffolding on your canvas.
Expand All @@ -31,10 +31,29 @@ Docker and `make` are all you need — Go and Bun are only required for

The UI is served on http://localhost:4000. Run `make help` for every target.

One thing to know on a fresh install: `EMAIL_SENDER_PROVIDER` defaults to `log`, so
every outbound mail is printed instead of sent. Signing up with a password does not
wait for verification — you land in the app straight away — but magic-link sign-in and
password reset need the link, and it is in `make docker-logs`. Point the sender at
SMTP, Resend or SendGrid when you have credentials.

To build and run everything from source instead, see
[CONTRIBUTING.md](.github/CONTRIBUTING.md) — the development workflow uses
`make local-docker-up`.

## Upgrading

Set `BLOCKNEXT_VERSION` in `.env` to the release you want (it defaults to `latest`),
then:

```bash
make docker-pull
make docker-up
```

Migrations run in their own container before the services start, so schema changes
are applied for you.

## Community

- [Architecture overview](ARCHITECTURE.md)
Expand Down
4 changes: 2 additions & 2 deletions apps/platform-api/internal/nodeengine/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ The node engine owns the **definitions** of everything a workflow can do: each n

## Core concepts

- **Node** (`domain/nodes/node.go`): a declarative action descriptor — `ID`, `Version`, name/description, `Icon` (a `Brand` naming the provider's mark plus a `Glyph` naming the action badge), `Inputs`/`Outputs` (the connection points, each a `Key` and an optional `Label`; every node states its own, there is no default), `InputSchema`/`OutputSchema` (`jsonschema-go`), `Categories`/`SubCategories`/`Tags`, `SupportedCredentials`, `Annotations`, `HasNaturalLanguage`, `Disabled`. Implements `NodeManager` (getter interface).
- **Node** (`domain/nodes/node.go`): a declarative action descriptor — `ID`, `Kind` (`action` for everything the runner executes, `note` for canvas-only nodes; every node states its own, there is no default), `Version`, name/description, `Icon` (a `Brand` naming the provider's mark plus a `Glyph` naming the action badge), `Inputs`/`Outputs` (the connection points, each a `Key` and an optional `Label`; every node states its own, there is no default), `InputSchema`/`OutputSchema` (`jsonschema-go`), `Categories`/`SubCategories`/`Tags`, `SupportedCredentials`, `Annotations`, `HasNaturalLanguage`, `Disabled`. Implements `NodeManager` (getter interface).

A node describes how it looks and how it connects, so the UI derives nothing: the artwork lives under `apps/platform/public/assets/icons/{brands,glyphs}/` and is composed at render time. `system_starter` declares an empty `Inputs` slice (no way in), `system_condition` declares two outputs labelled True and False. Credentials (`CredentialIcon`) and MCP servers (`ServerIcon`) name a `Brand` only — the glyph badge belongs to individual nodes.
A node describes how it looks and how it connects, so the UI derives nothing: the artwork lives under `apps/platform/public/assets/icons/{brands,glyphs}/` and is composed at render time. `system_starter` declares an empty `Inputs` slice (no way in), `system_condition` declares two outputs labelled True and False. `system_annotation` is the one `note` node: it is registered in the node catalog so both the UI and the flow generator can offer it, but it has no executor and is absent from the MCP `Tools` slice; `taskrunner.RunnableNodes` drops it before the DAG is built, so it never gets a node execution. Credentials (`CredentialIcon`) and MCP servers (`ServerIcon`) name a `Brand` only — the glyph badge belongs to individual nodes.
- **Executor** (`domain/executors/executor.go`): the behavior for a node, keyed by the same ID. `ExecutorManager.ExecuteWithContext(ctx, credentials, data)` is the execution entry point.
- **Credential** (`domain/credentials/credential.go`): a provider auth descriptor — schema, `IsOAuth1`/`IsOAuth2`/`IsSupportPlatform`, `SupportedNodes`. OAuth credentials may implement `RefreshableCredential`. `IsSupportPlatform` is auto-derived at load (host-provided OAuth apps).
- **Adapter** (`domain/adapters/adapter.go`): a trigger adapter that maps a raw webhook payload into a `TriggerContext` (`source`/`sender`/`prompt`/`payload`, exposed as `$trigger.*` variables).
Expand Down
6 changes: 6 additions & 0 deletions apps/platform-api/internal/nodeengine/domain/nodes/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

type Node struct {
ID string `json:"id,omitempty"`
Kind NodeKind `json:"kind"`
Version string `json:"version,omitempty"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Expand All @@ -25,6 +26,7 @@ type Node struct {

type NodeManager interface {
GetID() string
GetKind() NodeKind
GetVersion() string
GetName() string
GetDescription() string
Expand All @@ -48,6 +50,10 @@ func (n *Node) GetID() string {
return n.ID
}

func (n *Node) GetKind() NodeKind {
return n.Kind
}

func (n *Node) GetVersion() string {
return n.Version
}
Expand Down
12 changes: 12 additions & 0 deletions apps/platform-api/internal/nodeengine/domain/nodes/node_kind.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package nodes

type NodeKind string

const (
NodeKindAction NodeKind = "action"
NodeKindNote NodeKind = "note"
)

func (k NodeKind) String() string {
return string(k)
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ func NewAirtableCreateRecordNode(nodeID string) *AirtableCreateRecordNode {
return &AirtableCreateRecordNode{
Node: nodes.Node{
ID: nodeID,
Kind: nodes.NodeKindAction,
Version: "0.0.1",
Name: "Airtable Create Record",
Description: "Create a new record in an Airtable base.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func NewAirtableListRecordsNode(nodeID string) *AirtableListRecordsNode {
return &AirtableListRecordsNode{
Node: nodes.Node{
ID: nodeID,
Kind: nodes.NodeKindAction,
Version: "0.0.1",
Name: "Airtable List Records",
Description: "List records from an Airtable base.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func NewAnthropicChatNode(nodeID string) *AnthropicChatNode {
return &AnthropicChatNode{
Node: nodes.Node{
ID: nodeID,
Kind: nodes.NodeKindAction,
Version: "0.0.1",
Name: "Anthropic Chat",
Description: "Generate a chat completion using Anthropic Claude.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func NewChatgptChatNode(nodeID string) *ChatgptChatNode {
return &ChatgptChatNode{
Node: nodes.Node{
ID: nodeID,
Kind: nodes.NodeKindAction,
Version: "0.0.1",
Name: "ChatGPT Chat",
Description: "Generate a chat completion using OpenAI ChatGPT.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func NewCoingeckoAirdropTrackerNode(nodeID string) *CoingeckoAirdropTrackerNode
return &CoingeckoAirdropTrackerNode{
Node: nodes.Node{
ID: nodeID,
Kind: nodes.NodeKindAction,
Version: "0.0.1",
Name: "Coingecko Airdrop Tracker",
Description: "Track cryptocurrency airdrops via CoinGecko.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func NewCoingeckoPriceMonitorNode(nodeID string) *CoingeckoPriceMonitorNode {
return &CoingeckoPriceMonitorNode{
Node: nodes.Node{
ID: nodeID,
Kind: nodes.NodeKindAction,
Version: "0.0.1",
Name: "Coingecko Price Monitor",
Description: "Monitor cryptocurrency prices via CoinGecko.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func NewDeeplTranslateNode(nodeID string) *DeeplTranslateNode {
return &DeeplTranslateNode{
Node: nodes.Node{
ID: nodeID,
Kind: nodes.NodeKindAction,
Version: "0.0.1",
Name: "DeepL Translate",
Description: "Translate text using DeepL.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func NewDeepseekChatNode(nodeID string) *DeepseekChatNode {
return &DeepseekChatNode{
Node: nodes.Node{
ID: nodeID,
Kind: nodes.NodeKindAction,
Version: "0.0.1",
Name: "DeepSeek Chat",
Description: "Generate a chat completion using DeepSeek.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ func NewDiscordSendMediaNode(nodeID string) *DiscordSendMediaNode {
return &DiscordSendMediaNode{
Node: nodes.Node{
ID: nodeID,
Kind: nodes.NodeKindAction,
Version: "0.0.1",
Name: "Discord Send Media",
Description: "Send a media file to a Discord channel.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ func NewDiscordSendMessageNode(nodeID string) *DiscordSendMessageNode {
return &DiscordSendMessageNode{
Node: nodes.Node{
ID: nodeID,
Kind: nodes.NodeKindAction,
Version: "0.0.1",
Name: "Discord Send Message",
Description: "Send a text message to a Discord channel.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func NewElevenlabsTextToSpeechNode(nodeID string) *ElevenlabsTextToSpeechNode {
return &ElevenlabsTextToSpeechNode{
Node: nodes.Node{
ID: nodeID,
Kind: nodes.NodeKindAction,
Version: "0.0.1",
Name: "ElevenLabs Text To Speech",
Description: "Convert text to speech using ElevenLabs.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ func NewFacebookPublishMediaPostNode(nodeID string) *FacebookPublishMediaPostNod
return &FacebookPublishMediaPostNode{
Node: nodes.Node{
ID: nodeID,
Kind: nodes.NodeKindAction,
Version: "0.0.1",
Name: "Facebook Publish Media Post",
Description: "Publish a post with media to a Facebook page.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ func NewFacebookPublishPostNode(nodeID string) *FacebookPublishPostNode {
return &FacebookPublishPostNode{
Node: nodes.Node{
ID: nodeID,
Kind: nodes.NodeKindAction,
Version: "0.0.1",
Name: "Facebook Publish Post",
Description: "Publish a text post to a Facebook page.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ func NewFacebookPublishStoryNode(nodeID string) *FacebookPublishStoryNode {
return &FacebookPublishStoryNode{
Node: nodes.Node{
ID: nodeID,
Kind: nodes.NodeKindAction,
Version: "0.0.1",
Name: "Facebook Publish Story",
Description: "Publish a story to a Facebook page.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func NewGeminiChatNode(nodeID string) *GeminiChatNode {
return &GeminiChatNode{
Node: nodes.Node{
ID: nodeID,
Kind: nodes.NodeKindAction,
Version: "0.0.1",
Name: "Gemini Chat",
Description: "Generate a chat completion using Google Gemini.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func NewGeminiImageGenerationNode(nodeID string) *GeminiImageGenerationNode {
return &GeminiImageGenerationNode{
Node: nodes.Node{
ID: nodeID,
Kind: nodes.NodeKindAction,
Version: "0.0.1",
Name: "Gemini Nano Banana",
Description: "Generate an image from a text prompt using Google Gemini Nano Banana.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ func NewGoogleDocsCreateOrUpdateNode(nodeID string) *GoogleDocsCreateOrUpdateNod
return &GoogleDocsCreateOrUpdateNode{
Node: nodes.Node{
ID: nodeID,
Kind: nodes.NodeKindAction,
Version: "0.0.1",
Name: "Google Docs Create or Update",
Description: "Create a new Google Docs document or update an existing one with the provided content.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ func NewGoogleDocsReadDocsNode(nodeID string) *GoogleDocsReadDocsNode {
return &GoogleDocsReadDocsNode{
Node: nodes.Node{
ID: nodeID,
Kind: nodes.NodeKindAction,
Version: "0.0.1",
Name: "Google Docs Read",
Description: "Read the plain text content of a Google Docs document.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func NewGoogleDriveCreateFolderNode(nodeID string) *GoogleDriveCreateFolderNode
return &GoogleDriveCreateFolderNode{
Node: nodes.Node{
ID: nodeID,
Kind: nodes.NodeKindAction,
Version: "0.0.1",
Name: "Google Drive Create Folder",
Description: "Create a new folder in Google Drive.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func NewGoogleDriveCreateTextFileNode(nodeID string) *GoogleDriveCreateTextFileN
return &GoogleDriveCreateTextFileNode{
Node: nodes.Node{
ID: nodeID,
Kind: nodes.NodeKindAction,
Version: "0.0.1",
Name: "Google Drive Create Text File",
Description: "Create a plain text file in Google Drive.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ func NewGoogleDriveGetFileNode(nodeID string) *GoogleDriveGetFileNode {
return &GoogleDriveGetFileNode{
Node: nodes.Node{
ID: nodeID,
Kind: nodes.NodeKindAction,
Version: "0.0.1",
Name: "Google Drive Get File",
Description: "Retrieve a file from Google Drive along with its metadata.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func NewGoogleDriveUploadFileNode(nodeID string) *GoogleDriveUploadFileNode {
return &GoogleDriveUploadFileNode{
Node: nodes.Node{
ID: nodeID,
Kind: nodes.NodeKindAction,
Version: "0.0.1",
Name: "Google Drive Upload File",
Description: "Upload one or more files to Google Drive from a list of source URLs.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ func NewGmailOrganizeEmailsNode(nodeID string) *GmailOrganizeEmailsNode {
return &GmailOrganizeEmailsNode{
Node: nodes.Node{
ID: nodeID,
Kind: nodes.NodeKindAction,
Version: "0.0.1",
Name: "Gmail Organize Emails",
Description: "Organize Gmail messages by applying labels, stars, categories, read state, or trash actions to messages matching a search query.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ func NewGmailSearchEmailsNode(nodeID string) *GmailSearchEmailsNode {
return &GmailSearchEmailsNode{
Node: nodes.Node{
ID: nodeID,
Kind: nodes.NodeKindAction,
Version: "0.0.1",
Name: "Gmail Search Emails",
Description: "Search Gmail messages using Gmail's standard search syntax and return matching email summaries.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ func NewGmailSendEmailNode(nodeID string) *GmailSendEmailNode {
return &GmailSendEmailNode{
Node: nodes.Node{
ID: nodeID,
Kind: nodes.NodeKindAction,
Version: "0.0.1",
Name: "Gmail Send Email",
Description: "Send an email through Gmail.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func NewGoogleKeepCreateNoteNode(nodeID string) *GoogleKeepCreateNoteNode {
return &GoogleKeepCreateNoteNode{
Node: nodes.Node{
ID: nodeID,
Kind: nodes.NodeKindAction,
Version: "0.0.1",
Name: "Google Keep Create Note",
Description: "Create a new note in Google Keep.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ func NewGoogleKeepDeleteNoteNode(nodeID string) *GoogleKeepDeleteNoteNode {
return &GoogleKeepDeleteNoteNode{
Node: nodes.Node{
ID: nodeID,
Kind: nodes.NodeKindAction,
Version: "0.0.1",
Name: "Google Keep Delete Note",
Description: "Delete a note from Google Keep. This removes the note immediately and cannot be undone.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func NewGoogleKeepGetNoteNode(nodeID string) *GoogleKeepGetNoteNode {
return &GoogleKeepGetNoteNode{
Node: nodes.Node{
ID: nodeID,
Kind: nodes.NodeKindAction,
Version: "0.0.1",
Name: "Google Keep Get Note",
Description: "Get a single note from Google Keep by its resource name.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func NewGoogleKeepListNotesNode(nodeID string) *GoogleKeepListNotesNode {
return &GoogleKeepListNotesNode{
Node: nodes.Node{
ID: nodeID,
Kind: nodes.NodeKindAction,
Version: "0.0.1",
Name: "Google Keep List Notes",
Description: "List notes from Google Keep with optional filtering and pagination.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func NewGoogleSheetsAddDataToSpreadsheetNode(nodeID string) *GoogleSheetsAddData
return &GoogleSheetsAddDataToSpreadsheetNode{
Node: nodes.Node{
ID: nodeID,
Kind: nodes.NodeKindAction,
Name: "Google Sheets Add Data to Spreadsheet",
Version: "0.0.1",
Description: "Append rows of data to a Google Sheets spreadsheet.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ func NewGoogleSheetsCreateSpreadsheetNode(nodeID string) *GoogleSheetsCreateSpre
return &GoogleSheetsCreateSpreadsheetNode{
Node: nodes.Node{
ID: nodeID,
Kind: nodes.NodeKindAction,
Version: "0.0.1",
Name: "Google Sheets Create Spreadsheet",
Description: "Create a new Google Sheets spreadsheet with the given title.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ func NewGoogleSheetsDeleteSpreadsheetNode(nodeID string) *GoogleSheetsDeleteSpre
return &GoogleSheetsDeleteSpreadsheetNode{
Node: nodes.Node{
ID: nodeID,
Kind: nodes.NodeKindAction,
Version: "0.0.1",
Name: "Google Sheets Delete Spreadsheet",
Description: "Delete a Google Sheets spreadsheet by its identifier.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func NewGoogleSheetsReadSpreadsheetNode(nodeID string) *GoogleSheetsReadSpreadsh
return &GoogleSheetsReadSpreadsheetNode{
Node: nodes.Node{
ID: nodeID,
Kind: nodes.NodeKindAction,
Version: "0.0.1",
Name: "Google Sheets Read Spreadsheet",
Description: "Read rows from a Google Sheets spreadsheet, optionally filtering and limiting results.",
Expand Down
Loading