Compare commits

..
2 Commits
Author SHA1 Message Date
Ivor StrelarandClaude Opus 5 292abd93d8 fix: drop the HTTP endpoint that left 1.0.8 pending registry review
1.0.8 registered a ComfyUI server route to power a Refresh button, which
put that version into NodeVersionStatusPending and blocked installs. The
route only ever supplied a pre-run preview of the pending count; IS_CHANGED
already forces a fresh read of the sheet on every run, so removing it costs
no functionality.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 13:35:40 +02:00
Ivor StrelarandClaude Opus 5 10f6c59d48 feat: add SF Google Sheet Prompt Queue node
Dispatches unfinished prompts from a Google Sheet column as a batch, one
downstream graph run per prompt, capped by max_rows. Rows carrying any text
in the status column are skipped, so reruns continue where the last one
stopped and clearing a cell requeues that row. Scanning stops at the first
empty prompt cell.

Adds a Refresh button that re-reads the sheet and reports pending counts.

Also stops tracking node.zip, which is a `comfy node publish` build artifact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 13:19:57 +02:00
5 changed files with 225 additions and 1 deletions
+3
View File
@@ -36,6 +36,9 @@ env/
# Config with secrets
config.ini
# Publish artifact created by `comfy node publish`
node.zip
# OS
.DS_Store
Thumbs.db
+22
View File
@@ -154,10 +154,32 @@ Utility nodes for building and enhancing workflows.
| **SF Text Analyzer** | Analyze text content within a workflow. |
| **SF Google Sheet Cell** | Read a single cell from a publicly shared Google Sheet. Specify the sheet tab by GID, row by number (1-indexed), and column by letter (A, B, C, ...). |
| **SF Google Sheet Range** | Read a range of cells from a publicly shared Google Sheet and join them into a single string with a configurable delimiter. Supports multi-row, multi-column ranges. |
| **SF Google Sheet Prompt Queue** | Run a batch of prompts straight from a Google Sheet. Reads prompts from one column, skips any row you've marked as finished in a second column, and sends the rest downstream — the graph runs once per prompt. |
**Google Sheet nodes require the sheet to be shared as "Anyone with the link can view."**
The sheet tab GID is found in the browser URL after `#gid=` when you have that tab open.
#### Using SF Google Sheet Prompt Queue
Set your sheet up with prompts in one column and an empty column beside it for tracking:
| | A | B |
|---|---|---|
| **1** | Prompt | Status |
| **2** | a red cat on a windowsill | done |
| **3** | a blue dog in the snow | |
| **4** | a green bird mid-flight | |
With the defaults (`prompt_col` A, `status_col` B, `skip_first_row` on), a run sends rows 3 and 4
and skips row 2. Each prompt triggers a separate pass through the downstream graph.
- **`max_rows`** caps how many prompts go out per run — 10 by default.
- **Marking is manual.** Type anything into the status column — `done`, `x`, `executed` — and that
row is skipped next time. Clear the cell and it gets queued again.
- **Scanning stops at the first empty prompt cell**, so anything below a gap is ignored.
- **The sheet is re-read on every run**, so edits you make in the browser are picked up
automatically. Add rows or clear status cells and just hit Run again.
---
## Requirements
BIN
View File
Binary file not shown.
+199
View File
@@ -0,0 +1,199 @@
# ABOUTME: Dispatches prompts from a Google Sheet column, skipping rows already marked done
# ABOUTME: Status marks are written manually in the sheet; this node only reads
import csv
import io
import re
import requests
from .sf_google_sheet_cell import col_letter_to_index
def extract_spreadsheet_id(url: str) -> str:
match = re.search(r"/d/([a-zA-Z0-9-_]+)", url)
if match:
return match.group(1)
if re.match(r"^[a-zA-Z0-9-_]+$", url.strip()):
return url.strip()
raise ValueError(
"Could not extract spreadsheet ID. Please provide a full Google Sheets URL."
)
def fetch_rows(url: str, gid: str) -> list:
"""Reads a publicly shared sheet tab as a list of rows."""
spreadsheet_id = extract_spreadsheet_id(url)
gid = (gid or "0").strip() or "0"
csv_url = (
f"https://docs.google.com/spreadsheets/d/{spreadsheet_id}"
f"/gviz/tq?tqx=out:csv&gid={gid}"
)
try:
response = requests.get(csv_url, timeout=15)
response.raise_for_status()
except requests.exceptions.HTTPError as e:
raise RuntimeError(
f"Could not access Google Sheet (HTTP {response.status_code}). "
f"Make sure it is shared as 'Anyone with the link can view'. Error: {e}"
)
except requests.exceptions.RequestException as e:
raise RuntimeError(f"Failed to fetch Google Sheet: {e}")
return list(csv.reader(io.StringIO(response.content.decode("utf-8"))))
def scan_rows(rows, prompt_idx, status_idx, skip_first_row, max_rows=None):
"""Walks the sheet and returns (pending, total).
`pending` is [(sheet_row_number, prompt), ...] for rows whose status cell is
empty, capped at `max_rows` when given. `total` counts every prompt found.
Scanning stops at the first row with an empty prompt cell.
"""
pending = []
total = 0
start = 1 if skip_first_row else 0
for offset in range(start, len(rows)):
row = rows[offset]
prompt = row[prompt_idx].strip() if prompt_idx < len(row) else ""
if not prompt:
break
total += 1
status = row[status_idx].strip() if status_idx < len(row) else ""
if status:
continue
if max_rows is None or len(pending) < max_rows:
pending.append((offset + 1, prompt))
return pending, total
class SFGoogleSheetPromptQueue:
"""
Sends unfinished prompts from a Google Sheet downstream as a batch.
Reads prompts from one column and checks a second column for a completion
mark. Rows with anything written in the status column are skipped, so each
run picks up where the last one left off. Clearing a status cell in the
browser queues that row again.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"url": (
"STRING",
{
"multiline": False,
"default": "https://docs.google.com/spreadsheets/d/.../edit",
"tooltip": (
"Full Google Sheets URL. "
"The sheet must be shared: Share → Anyone with the link → Viewer."
),
},
),
"sheet_gid": (
"STRING",
{
"default": "0",
"tooltip": (
"Sheet tab GID — the number after '#gid=' in the URL. "
"Use 0 for the first tab."
),
},
),
"prompt_col": (
"STRING",
{"default": "A", "tooltip": "Column holding the prompts (e.g. A)."},
),
"status_col": (
"STRING",
{
"default": "B",
"tooltip": (
"Column you mark by hand when a prompt is finished. "
"Any text at all (done, x, executed) means skip that row."
),
},
),
"skip_first_row": (
"BOOLEAN",
{
"default": True,
"tooltip": "Ignore row 1, which is usually a header.",
},
),
"max_rows": (
"INT",
{
"default": 10,
"min": 1,
"max": 500,
"step": 1,
"tooltip": "Most prompts to send in a single run.",
},
),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("prompt",)
OUTPUT_IS_LIST = (True,)
FUNCTION = "dispatch"
CATEGORY = "Stillfront/Utils"
DESCRIPTION = """
Sends every unfinished prompt from a Google Sheet column downstream — the graph
runs once per prompt, up to **max_rows** prompts per run.
A row is treated as finished when its **status_col** cell contains any text at
all. Write `done` next to a prompt in the sheet and the next run skips it; clear
the cell and it gets queued again. Scanning stops at the first empty prompt cell.
The sheet is re-read fresh on every run, so edits you make in the browser are
picked up automatically — there is nothing to refresh by hand.
"""
@classmethod
def IS_CHANGED(cls, **kwargs):
# The sheet changes outside ComfyUI, so never serve a cached result.
return float("nan")
def dispatch(self, url, sheet_gid, prompt_col, status_col, skip_first_row, max_rows):
prompt_idx = col_letter_to_index(prompt_col)
status_idx = col_letter_to_index(status_col)
if prompt_idx == status_idx:
raise ValueError(
f"prompt_col and status_col are both '{prompt_col.upper()}'. They must differ."
)
rows = fetch_rows(url, sheet_gid)
pending, total = scan_rows(rows, prompt_idx, status_idx, skip_first_row, max_rows)
if not pending:
if total == 0:
raise ValueError(
f"No prompts found in column {prompt_col.upper()}. Check the column "
f"letter, the sheet GID, and the skip_first_row setting."
)
raise ValueError(
f"Nothing left to run — all {total} prompt(s) in column "
f"{prompt_col.upper()} are marked in column {status_col.upper()}. "
f"Clear those cells in the sheet to queue them again."
)
print(
f"[SF Google Sheet Prompt Queue] Sending {len(pending)} of {total} prompt(s), "
f"rows {pending[0][0]}-{pending[-1][0]}."
)
return ([prompt for _, prompt in pending],)
NODE_CLASS_MAPPINGS = {"SFGoogleSheetPromptQueue": SFGoogleSheetPromptQueue}
NODE_DISPLAY_NAME_MAPPINGS = {"SFGoogleSheetPromptQueue": "SF Google Sheet Prompt Queue"}
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-sf-nodes"
description = "Custom nodes for ComfyUI including LLM Chat with support for Claude, Gemini, and OpenAI models, WaveSpeed AI image/video generation, VertexAI integration, and workflow utilities"
version = "1.0.7"
version = "1.0.9"
license = { file = "LICENSE" }
dependencies = [
"requests>=2.31.0",