<div v-pre>

# Workflow Node Catalog

::: warning Implementation Status
This catalog defines all node types available in the **workflow builder canvas** (UI palette). However, only the following node types are currently **executable at runtime**:

- **Lens** — fully implemented via the execution provider registry
- **Lens Execute** — alias for Lens node execution

All other node types (Logic, Data, AI Primitives, Storage/IO, Communication, Integrations, Battle/Arena, Media) are **designed and documented** but do not yet have runtime execution implementations. They appear in the canvas palette for workflow design and planning purposes.

The execution engine currently supports DAG validation (cycle detection, binding completeness) and single-provider-per-node execution via the provider registry (`openai`, `anthropic`, `google`, `mistral`, `ollama`, `fal-ai`, `research`, `pdf-export`). Multi-node orchestration with inter-node data flow is scheduled for implementation.

**Status key:** 🟢 Executable | 🟡 Schema & validation only | 🔴 Design-only (no runtime)
:::

This is the human reference for the typed catalog in `libs/infra/execution/src/lib/catalog/workflow-node-catalog.ts`.
Every row includes the requested structure: Node, Purpose, Inputs, Outputs, Required Configuration, Optional Configuration, Example Configuration, Common Valid Connections, Common Invalid Connections, and Execution Notes.

## Lenses 🟢

| Node | Purpose | Inputs | Outputs | Required Configuration | Optional Configuration | Example Configuration | Common Valid Connections | Common Invalid Connections | Execution Notes |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Lens | Execute a LenserFight lens prompt with model and funding controls. | text or json prompt context. | text result with model metadata. | `model_id`. | `param_overrides`, `funding_source`, `key_ref_id`, `local_key_id`, retry policy. | `model_id: openai:gpt-4.1-mini`, `funding_source: platform_credit`, `param_overrides: { tone: "concise" }`; input `{ text: "Summarize weekly battle results" }`; output `{ text: "Weekly digest..." }`; downstream `Email Send.body = $.text`. | Prompt Template -> Lens, RAG Retriever -> Lens, Lens -> Output Parser, Lens -> Email Send. | Embedding -> Lens without query text. | Existing workflows keep `model_id` and `param_overrides`; execution normalizes `model_id` to `modelId`. |

## Triggers 🟡

| Node | Purpose | Inputs | Outputs | Required Configuration | Optional Configuration | Example Configuration | Common Valid Connections | Common Invalid Connections | Execution Notes |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Manual Trigger | Start a workflow manually with typed root inputs. | none. | json `rootInputs`. | none. | `inputSchema`. | `inputSchema: { query: { type: "string", required: true } }`; input `{}`; output `{ rootInputs: { query: "Which strategy won?" } }`; downstream `RAG Retriever.query = $.rootInputs.query`. | Manual Trigger -> RAG Retriever, Manual Trigger -> HTTP Request. | Manual Trigger -> Image Upscale without image input. | Browser and worker safe; production publishing can reuse the same input schema. |
| Schedule Trigger | Start a workflow on a cron schedule. | none. | json schedule metadata. | `cron`. | `timezone`, `enabled`. | `cron: "0 8 * * MON"`, `timezone: "Europe/Istanbul"`; input `{}`; output `{ firedAt: "2026-05-18T08:00:00+03:00" }`; downstream `Supabase Query.params.since = $.firedAt`. | Schedule Trigger -> Supabase Query, Schedule Trigger -> RSS Feed. | Schedule Trigger -> Output Parser without a text transform. | Scheduled execution environment; draft edits should not affect published schedule until publish. |
| Webhook Trigger | Start a workflow from an inbound HTTP request. | none. | json `{ body, headers, method }`. | `path`. | `method`, `secretRef`, response mode. | `path: "/hooks/github-pr-review"`, `method: "POST"`, `secretRef: "github-webhook-secret"`; output `{ body: { action: "opened", pull_request: { number: 42 } } }`; downstream `GitHub PR Review.prNumber = $.body.pull_request.number`. | Webhook Trigger -> Switch, Webhook Trigger -> GitHub PR Review. | Webhook Trigger -> Email Send without mapping subject/body. | Server or worker only; validate signature before execution. |
| Event Trigger | Start from a LenserFight domain event. | none. | json event envelope. | `eventType`. | `workspaceId`, filters. | `eventType: "battle.completed"`, `workspaceId: "{{workspace.id}}"`; output `{ eventType: "battle.completed", payload: { winner: "sourced" } }`; downstream `Slack Notify.text = $.payload.winner`. | Event Trigger -> Slack Notify, Event Trigger -> Leaderboard Update. | Event Trigger -> Speech To Text without audio. | Worker/server event bus; useful for production automation. |
| Form / Input Trigger | Start from a rendered user form. | none. | json submitted fields. | `fields`. | submitter policy, validation messages. | `fields: [{ key: "prompt", type: "textarea", required: true }, { key: "contenders", type: "array", required: true }]`; output `{ fields: { prompt: "Judge these answers", contenders: ["concise","sourced"] } }`; downstream `Battle Execute.prompt = $.fields.prompt`. | Form Trigger -> Battle Execute, Form Trigger -> Prompt Template. | Form Trigger -> Vector Search without embedding. | Browser entry point; validation runs before DAG execution. |

## Logic 🔴

| Node | Purpose | Inputs | Outputs | Required Configuration | Optional Configuration | Example Configuration | Common Valid Connections | Common Invalid Connections | Execution Notes |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Code | Run sandboxed JavaScript/TypeScript against upstream data. | any. | json result. | `code`. | `timeoutMs`. | `code: "return { digestText: input.summary, itemCount: input.items.length }"`, `timeoutMs: 5000`; output `{ result: { digestText: "3 battles changed rank", itemCount: 12 } }`; downstream `Email Send.body = $.result.digestText`. | Any -> Code, Code -> Email Send, Code -> Data Mapper. | Code -> Speech To Text without audio output. | Browser/worker sandbox; keep deterministic and time-limited. |
| Switch | Route execution based on matching cases. | any. | text `branch`. | `cases`. | `inputPath`, `defaultBranch`. | `inputPath: "$.status"`, `cases: [{ label: "failed", operator: "equals", value: "failed" }]`, `defaultBranch: "ok"`; output `{ branch: "failed" }`; downstream `Slack Notify.text = $.branch`. | Webhook Trigger -> Switch, Classifier -> Switch. | Switch -> Embedding without branch text mapping. | Edge labels can represent branch names in a later UI pass. |
| If / Condition | Continue when a boolean condition passes. | any. | boolean `passed`. | `condition`. | else branch label. | `condition: "$.score >= 0.8"`; output `{ passed: true }`; downstream `Leaderboard Update.approved = $.passed`. | Judge / Eval -> If, If -> Slack Notify. | If -> Image Analyze without image. | Warning edges stay editor-only; execution uses branch evaluation. |
| Loop / Map | Iterate or map array items. | array or json. | array items. | none. | `arrayPath`, `itemVariable`, `maxItems`. | `arrayPath: "$.documents"`, `itemVariable: "document"`, `maxItems: 25`; output `{ items: [{ title: "Battle recap", score: 0.91 }] }`; downstream `Summarizer.documents = $.items`. | RSS Feed -> Loop / Map, Loop / Map -> Summarizer. | Loop / Map -> Email Send without aggregation. | Use Split In Batches for rate-sensitive APIs. |
| Wait / Delay | Pause execution until a duration or timestamp. | any. | json resume metadata. | none. | `delayMs`, `delayUntil`. | `delayMs: 300000`; output `{ resumedAt: "2026-05-16T09:05:00Z" }`; downstream `GitHub Read.since = $.resumedAt`. | Schedule Trigger -> Wait, Wait -> HTTP Request. | Wait -> Output Parser without text. | Worker/server execution should persist waiting state. |
| Error Catch | Convert upstream errors into a fallback path. | error. | json recovery payload. | none. | `fallbackValue`, `continueOnError`. | `fallbackValue: { alert: "RSS summarization failed" }`, `continueOnError: true`; output `{ recovery: { alert: "RSS summarization failed" } }`; downstream `Slack Notify.text = $.recovery.alert`. | Any failing node -> Error Catch, Error Catch -> Slack Notify. | Error Catch -> Embedding without text/documents. | Does not hide errors unless explicitly configured. |
| Try / Catch | Protect a branch and expose result or error. | any. | json `{ ok, result, error }`. | none. | `catchBranch`. | `catchBranch: "notify_failure"`; output `{ ok: false, error: { message: "Provider timeout" } }`; downstream `Slack Notify.text = $.error.message`. | HTTP Request -> Try / Catch, Try / Catch -> Slack Notify. | Try / Catch -> Vector Search without embedding. | Useful for production workflows with fallback notifications. |
| Merge | Merge multiple upstream branches. | any. | json merged payload. | none. | `strategy`. | `strategy: "json_object"`; output `{ merged: { digest: "Ready", scores: [0.8, 0.9] } }`; downstream `Email Send.body = $.merged.digest`. | Switch branches -> Merge, Merge -> Email Send. | Merge -> Speech To Text without audio. | Supports `last_write_wins`, `concat`, `array`, `json_object`. |
| Split In Batches | Process arrays in smaller batches. | array. | array batch. | none. | `batchSize`. | `batchSize: 10`; output `{ batch: [{ url: "https://example.com/feed/1" }] }`; downstream `RSS Feed.urls = $.batch`. | Sheets Read -> Split In Batches, Split In Batches -> HTTP Request. | Split In Batches -> Email Send without summary. | Use with retry/rate limit for external APIs. |
| Sub-Workflow | Invoke another workflow with mapped inputs. | json. | workflow_result. | `workflowId`. | `inputMapping`, `maxDepth`. | `workflowId: "wf_weekly_digest"`, `inputMapping: { topic: "$.topic", window: "$.window" }`, `maxDepth: 2`; output `{ workflowResult: { status: "completed", output: "Digest ready" } }`; downstream `Email Send.body = $.workflowResult.output`. | Parent workflow -> Sub-Workflow, Sub-Workflow -> Stop / Return. | Sub-Workflow -> Image Upscale without image. | Guard recursion depth. |
| Stop / Return | End execution and return a final payload. | any. | workflow_result. | none. | `returnPath`, `status`. | `returnPath: "$.answer"`, `status: "completed"`; output `{ workflowResult: { status: "completed", output: "Approved answer" } }`; downstream `Logger.message = $.workflowResult.output`. | Any terminal branch -> Stop / Return. | Stop / Return -> required downstream production action. | Usually terminal; downstream Logger is only for debugging. |

## Data 🔴

| Node | Purpose | Inputs | Outputs | Required Configuration | Optional Configuration | Example Configuration | Common Valid Connections | Common Invalid Connections | Execution Notes |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| JSON Transform | Transform JSON with an expression. | json. | json. | `expression`. | source path. | `expression: "{ title: input.title, score: input.score }"`; output `{ transformed: { title: "Lens A", score: 0.92 } }`; downstream `Data Mapper.input = $`. | Webhook Trigger -> JSON Transform, JSON Transform -> Data Mapper. | JSON Transform -> Email Send without subject/body mapping. | Prefer structured mapping over string parsing. |
| Set Variables | Set workflow variables. | any. | json variables. | `variables`. | scope. | `variables: { digestWindow: "7d", channel: "#arena-alerts" }`; output `{ variables: { digestWindow: "7d" } }`; downstream `Prompt Template.variables = $.variables`. | Manual Trigger -> Set Variables, Set Variables -> Prompt Template. | Set Variables -> Image Analyze without image. | Variable mutations are visible downstream. |
| Extract Field | Extract one field. | json. | text. | `path`. | default value. | `path: "$.pull_request.body"`; output `{ value: "This PR adds workflow runners." }`; downstream `Summarizer.content = $.value`. | GitHub Read -> Extract Field, Extract Field -> Summarizer. | Extract Field -> Vector Search without embedding. | Use before Email Send subject/body mappings. |
| Rename Field | Rename payload fields. | json. | json. | `renames`. | preserve unknown fields. | `renames: { body: "digestBody", title: "digestTitle" }`; output `{ digestBody: "...", digestTitle: "Weekly digest" }`; downstream `Email Send.body = $.digestBody`. | JSON Transform -> Rename Field, Rename Field -> Email Send. | Rename Field -> Speech To Text without audio. | Non-destructive by default. |
| Filter Items | Filter array items. | array. | array. | `condition`. | limit, empty behavior. | `condition: "$.score >= 0.75"`; output `{ items: [{ id: "doc_1", score: 0.91 }] }`; downstream `RAG Retriever.filters = $.items`. | Sheets Read -> Filter Items, Filter Items -> Aggregate. | Filter Items -> Email Send without summarizer. | Keep filtered output shape stable. |
| Aggregate | Aggregate grouped values. | array. | json. | `groupBy`. | metrics. | `groupBy: "$.contender"`, `metrics: [{ field: "$.score", op: "avg", as: "averageScore" }]`; output `{ groups: [{ contender: "A", averageScore: 0.84 }] }`; downstream `Score Aggregator.input = $.groups`. | Vote Collector -> Aggregate, Aggregate -> Score Aggregator. | Aggregate -> Embedding without text/documents. | Useful before reports and leaderboards. |
| Sort | Sort items. | array. | array. | `sortBy`. | `direction`. | `sortBy: "$.score"`, `direction: "desc"`; output `{ items: [{ id: "a", score: 0.98 }] }`; downstream `Leaderboard Update.scores = $.items`. | Aggregate -> Sort, Sort -> Leaderboard Update. | Sort -> Output Parser without text. | Stable sort recommended for ties. |
| Deduplicate | Remove duplicate items. | array. | array. | `keyPath`. | keep strategy. | `keyPath: "$.url"`; output `{ items: [{ url: "https://example.com/post", title: "Arena news" }] }`; downstream `Summarizer.documents = $.items`. | RSS Feed -> Deduplicate, Deduplicate -> Summarizer. | Deduplicate -> Text To Speech without text. | Use before expensive AI calls. |
| Text Splitter | Chunk long text/documents. | text. | document[]. | `chunkSize`. | `chunkOverlap`. | `chunkSize: 1000`, `chunkOverlap: 120`; output `{ documents: [{ pageContent: "Battle report chunk...", metadata: { chunk: 1 } }] }`; downstream `Embedding.documents = $.documents`. | File Reader -> Text Splitter, Text Splitter -> Embedding. | Text Splitter -> Email Send without summarizer. | Required bridge for RAG indexing. |
| Data Mapper | Map fields into target schema. | any. | json. | `mapping`. | defaults. | `mapping: { to: "$.owner.email", subject: "PR Review: {{title}}", body: "$.summary" }`; output `{ to: "owner@example.com", subject: "PR Review: Add runners", body: "Review summary..." }`; downstream `Email Send = $`. | Embedding -> Data Mapper, Data Mapper -> Email Send. | Data Mapper -> Image Upscale without image URL. | Primary bridge for incompatible edges. |

## AI Primitives 🔴

| Node | Purpose | Inputs | Outputs | Required Configuration | Optional Configuration | Example Configuration | Common Valid Connections | Common Invalid Connections | Execution Notes |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Prompt Template | Render a prompt. | any. | text prompt. | `template`. | `variables`. | `template: "Summarize these results for {{audience}}: {{results}}"`, `variables: { audience: "founders", results: "$.results" }`; output `{ prompt: "Summarize..." }`; downstream `Lens Execute.prompt = $.prompt`. | RAG Retriever -> Prompt Template, Prompt Template -> Lens Execute. | Prompt Template -> Image Upscale without image. | Safe browser render; no provider call. |
| Lens Execute | Execute a selected lens as a utility node. | text. | lens_result. | `lensId`, `model_id`. | `param_overrides`, funding fields. | `lensId: "lens_weekly_digest"`, `model_id: "openai:gpt-4.1-mini"`, `param_overrides: { tone: "crisp" }`; output `{ lensResult: { text: "Weekly digest..." } }`; downstream `Email Send.body = $.lensResult.text`. | Prompt Template -> Lens Execute, Lens Execute -> Output Parser. | Embedding -> Lens Execute without text query. | Preserves legacy lens workflow compatibility. |
| Agent Execute | Delegate to a configured agent. | text. | agent_result. | `agentId`, `task`. | `delegationPolicy`. | `agentId: "agent_pr_reviewer"`, `task: "Review PR {{prNumber}} for security and tests"`, `delegationPolicy: "approval_required"`; output `{ agentResult: { status: "completed", summary: "Found 2 issues" } }`; downstream `GitHub PR Review.reviewBody = $.agentResult.summary`. | GitHub Read -> Agent Execute, Agent Execute -> GitHub PR Review. | Agent Execute -> Image Analyze without image. | Approval policy can block delegation. |
| Output Parser | Parse model text into JSON. | text. | json. | none. | `schema`, `strict`. | `schema: { score: "number", reasoning: "string", winner: "string" }`, `strict: true`; output `{ score: 0.86, winner: "candidate_a" }`; downstream `Judge / Eval.candidates = $`. | Lens -> Output Parser, Output Parser -> Email Send. | Embedding -> Output Parser. | Bridge from text to structured config. |
| Embedding | Convert text/documents into vectors. | text or document[]. | embedding vector with metadata. | `provider`, `model`. | `inputPath`, `chunkSize`, `dimensions`, `metadataFields`, retry. | `provider: "openai"`, `model: "text-embedding-3-small"`, `inputPath: "$.documents[*].pageContent"`, `chunkSize: 1000`, `dimensions: 1536`, `metadataFields: ["battleId","contenderId"]`; output `{ embedding: { vector: [0.013,-0.024,0.071], dimensions: 1536 } }`; downstream `Vector Search.vector = $.embedding.vector`. | Text Splitter -> Embedding, Embedding -> Vector Search. | Embedding -> Email Send. | Requires provider funding; not human-readable until mapped. |
| RAG Retriever | Retrieve scored documents. | text query or embedding. | document[]. | `vectorStore`, `queryPath`. | `topK`, `similarityThreshold`, `filters`, `rerank`. | `vectorStore: "supabase:workflow_documents"`, `queryPath: "$.rootInputs.query"`, `topK: 6`, `similarityThreshold: 0.74`, `filters: { workspaceId: "{{workspace.id}}" }`, `rerank: true`; output `{ documents: [{ pageContent: "Rubric...", score: 0.91 }] }`; downstream `Prompt Template.context = $.documents[*].pageContent`. | Manual Trigger -> RAG Retriever, RAG Retriever -> Prompt Template. | RAG Retriever -> Email Send without summarizer/template. | Worker/provider funding; filters must match vector schema. |
| Vector Search | Search vector index directly. | embedding. | document[]. | `vectorStore`. | `topK`, filters. | `vectorStore: "supabase:workflow_documents"`, `topK: 5`, `filters: { battleId: "$.metadata.battleId" }`; output `{ documents: [{ id: "doc_1", score: 0.88 }] }`; downstream `Summarizer.documents = $.documents`. | Embedding -> Vector Search, Vector Search -> Summarizer. | Vector Search -> Email Send without text conversion. | Use RAG Retriever for query-text workflows. |
| Judge / Eval | Score candidates against a rubric. | json candidates plus rubric text. | json score, reasoning, winner, confidence. | `rubric`, `scoringScale`. | `judgeModel`, `tieBreakRule`, `outputParser`, `confidenceThreshold`. | `rubric: "Score correctness, source coverage, actionability"`, `scoringScale: "0-100"`, `judgeModel: "anthropic:claude-3-7-sonnet"`, `tieBreakRule: "prefer_higher_source_coverage"`, `confidenceThreshold: 0.76`; output `{ score: 91, winner: "answerA", reasoning: "More complete", confidence: 0.84 }`; downstream `Score Aggregator.score = $.score`. | Output Parser -> Judge / Eval, Judge / Eval -> Score Aggregator. | Embedding -> Judge / Eval without candidate mapping. | Provider-funded; should use strict parser for production. |
| Memory Read | Retrieve workflow memory. | any query context. | document[]. | `memoryKey`. | `limit`, query. | `memoryKey: "workspace:arena-digests"`, `limit: 8`, `query: "$.query"`; output `{ documents: [{ pageContent: "Last digest favored concise answers" }] }`; downstream `Prompt Template.memory = $.documents`. | Manual Trigger -> Memory Read, Memory Read -> Prompt Template. | Memory Read -> Email Send without summary. | Server/worker memory store. |
| Memory Write | Persist workflow memory. | any. | json write status. | `memoryKey`, `contentPath`. | `policy`, metadata. | `memoryKey: "workspace:arena-digests"`, `contentPath: "$.summary"`, `policy: "checkpoint"`, `metadata: { source: "weekly_digest" }`; output `{ written: true }`; downstream `Logger.message = $.memoryKey`. | Summarizer -> Memory Write, Memory Write -> Logger. | Memory Write -> Embedding without content path. | `on_success` avoids storing failed-run state. |
| Chain | Run a configured AI chain. | any. | json. | `steps`. | chain timeout. | `steps: [{ type: "prompt_template", template: "Summarize {{input}}" }, { type: "lens_execute", model_id: "openai:gpt-4.1-mini" }, { type: "output_parser", schema: { summary: "string" } }]`; output `{ summary: "PR changes add catalog-backed nodes" }`; downstream `Slack Notify.text = $.summary`. | RAG Retriever -> Chain, Chain -> Slack Notify. | Chain -> Speech To Text without audio. | Good for reusable AI pipelines; keep step count bounded. |
| Summarizer | Summarize text/documents. | text or document[]. | text. | `model`. | `provider`, `inputPath`, instructions. | `provider: "openai"`, `model: "gpt-4.1-mini"`, `inputPath: "$.documents"`, `instructions: "Create weekly digest email"`; output `{ text: "Generated summary..." }`; downstream `Email Send.body = $.text`. | RSS Feed -> Summarizer, Summarizer -> Email Send. | Summarizer -> Vector Search without embedding. | Provider-funded. |
| Classifier | Classify text into labels. | text. | json label/confidence. | `model`. | labels, threshold. | `model: "gpt-4.1-mini"`, `labels: ["security","tests","docs"]`, `inputPath: "$.reviewBody"`; output `{ label: "security", confidence: 0.87 }`; downstream `Switch.inputPath = $.label`. | GitHub Read -> Classifier, Classifier -> Switch. | Classifier -> Image Upscale without image. | Use threshold before auto-actions. |
| Translator | Translate text. | text. | text. | `model`, target language. | source language, tone. | `model: "gpt-4.1-mini"`, `targetLanguage: "Turkish"`, `inputPath: "$.founderNote"`; output `{ text: "Yerellestirilmis not..." }`; downstream `Notion Write.Summary = $.text`. | Lens -> Translator, Translator -> Notion Write. | Translator -> Embedding when untranslated documents are required. | Preserve locale metadata. |
| Image Analyze | Analyze an image. | image. | json observations. | `model`. | schema, moderation. | `provider: "openai"`, `model: "gpt-4.1-mini"`, `imagePath: "$.image.url"`, `schema: { safety: "string", caption: "string" }`; output `{ caption: "Arena scorecard", safety: "ok" }`; downstream `Judge / Eval.candidates = $`. | Object Storage Download -> Image Analyze, Image Analyze -> Judge / Eval. | Text Splitter -> Image Analyze. | Requires image-capable provider. |
| Audio Transcribe | Transcribe audio. | audio. | text. | `model`. | language, timestamps. | `provider: "openai"`, `model: "gpt-4o-transcribe"`, `audioPath: "$.audio.url"`, `timestamps: true`; output `{ text: "The battle winner is..." }`; downstream `Summarizer.content = $.text`. | File Reader -> Audio Transcribe, Audio Transcribe -> Summarizer. | Audio Transcribe -> Image Upscale. | Media artifact should be durable before worker execution. |
| Video Analyze | Analyze video. | video. | json notes. | `model`. | frame sampling, transcript. | `provider: "openai"`, `model: "gpt-4.1-mini"`, `videoPath: "$.video.url"`, `sampleEverySeconds: 5`; output `{ scenes: ["scoreboard"], summary: "Battle replay..." }`; downstream `Summarizer.content = $.summary`. | Object Storage Download -> Video Analyze, Video Analyze -> Summarizer. | Video Analyze -> Embedding without text extraction. | Long videos require worker/server execution. |

## Battle / Arena 🔴

| Node | Purpose | Inputs | Outputs | Required Configuration | Optional Configuration | Example Configuration | Common Valid Connections | Common Invalid Connections | Execution Notes |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Battle Create | Create a battle definition. | json. | json battle id/status. | `battleId` or title/contenders. | visibility. | `title: "RAG answer showdown"`, `visibility: "workspace"`; output `{ battleId: "battle_123", status: "draft" }`; downstream `Battle Execute.battleId = $.battleId`. | Form Trigger -> Battle Create, Battle Create -> Battle Execute. | Battle Create -> Email Send without result text. | Server-side write path. |
| Battle Execute | Run contenders and judge strategy. | text prompt plus array contenders. | battle_result. | `contenders`, `judgeStrategy`. | `promptSource`, `fundingSource`, `resultVisibility`, retry. | `contenders: [{ id: "concise", lensId: "lens_concise_answer" }, { id: "sourced", lensId: "lens_sourced_answer" }]`, `promptSource: "$.prompt"`, `judgeStrategy: "panel"`, `fundingSource: "platform_credit"`, `resultVisibility: "workspace"`; output `{ battleId: "battle_123", winner: "sourced", scores: { sourced: 94, concise: 78 } }`; downstream `Leaderboard Update.scores = $.scores`. | Form Trigger -> Battle Execute, Battle Execute -> Leaderboard Update. | Embedding -> Battle Execute without prompt/contenders. | Provider-funded and should publish result visibility explicitly. |
| Contender Run | Run a single contender. | text prompt. | lens_result. | `contenderId`. | `lensId`, `model_id`. | `contenderId: "sourced"`, `lensId: "lens_sourced_answer"`, `model_id: "openai:gpt-4.1-mini"`; output `{ contenderId: "sourced", output: "Answer with citations..." }`; downstream `Judge Battle.candidates = $`. | Prompt Template -> Contender Run, Contender Run -> Judge Battle. | Contender Run -> Vector Search without embedding. | Use inside battle orchestration. |
| Judge Battle | Judge contender outputs. | json. | battle_result. | `rubric`. | `judgeModel`, tie rules. | `rubric: "Prefer correctness and source coverage"`, `judgeModel: "anthropic:claude-3-7-sonnet"`; output `{ winner: "sourced", scores: { sourced: 94, concise: 78 } }`; downstream `Score Aggregator.input = $`. | Contender Run -> Judge Battle, Judge Battle -> Score Aggregator. | Judge Battle -> Image Analyze without image. | Similar to Judge / Eval but battle-specific. |
| Vote Collector | Collect votes. | json battle context. | json votes. | `battleId`. | `closeAfterVotes`, voters. | `battleId: "$.battleId"`, `closeAfterVotes: 7`; output `{ votes: [{ voter: "u1", contenderId: "sourced" }] }`; downstream `Score Aggregator.votes = $.votes`. | Battle Execute -> Vote Collector, Vote Collector -> Score Aggregator. | Vote Collector -> Embedding without text. | Human-in-the-loop friendly. |
| Score Aggregator | Aggregate scores/votes. | json. | battle_result. | `aggregation`. | weights. | `aggregation: "weighted_average"`, `weights: { judge: 0.8, vote: 0.2 }`; output `{ winner: "sourced", finalScore: 91.6 }`; downstream `Leaderboard Update.score = $.finalScore`. | Judge Battle -> Score Aggregator, Vote Collector -> Score Aggregator. | Score Aggregator -> Speech To Text without audio. | Keep scoring scale explicit. |
| Leaderboard Update | Update arena leaderboard. | battle_result. | json update status. | `leaderboardId`. | `scorePath`, visibility. | `leaderboardId: "arena_weekly"`, `scorePath: "$.finalScore"`, `visibility: "workspace"`; output `{ updated: true, rank: 1 }`; downstream `Slack Notify.text = $.rank`. | Battle Execute -> Leaderboard Update, Leaderboard Update -> Slack Notify. | Leaderboard Update -> Output Parser without text. | Server-side mutation; require workspace permissions. |

## Storage And I/O 🔴

| Node | Purpose | Inputs | Outputs | Required Configuration | Optional Configuration | Example Configuration | Common Valid Connections | Common Invalid Connections | Execution Notes |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Supabase Query | Run allowlisted Supabase RPC/query. | json params. | json rows/data. | `rpcName`. | `params`, timeout, retry. | `rpcName: "workflows.fn_recent_battle_results"`, `params: { window: "7d" }`; output `{ rows: [{ battleId: "battle_123", winner: "sourced" }] }`; downstream `Aggregate.items = $.rows`. | Schedule Trigger -> Supabase Query, Supabase Query -> Aggregate. | Supabase Query -> Speech To Text without audio. | Only allowlisted RPCs; review RLS/grants before production. |
| SQL Query | Run parameterized SQL. | json params. | json rows. | `query`. | connection, timeout. | `query: "select title,winner from arena.battles where created_at >= :since"`, `params: { since: "$.firedAt" }`; output `{ rows: [{ title: "RAG showdown", winner: "sourced" }] }`; downstream `Data Mapper.input = $.rows`. | Schedule Trigger -> SQL Query, SQL Query -> Data Mapper. | SQL Query -> Image Analyze without image. | Server-only; avoid raw user SQL. |
| KV Read | Read workflow KV. | any. | json value/found. | `key`. | namespace. | `key: "digest:lastSuccessfulRun"`; output `{ value: "2026-05-09T08:00:00Z", found: true }`; downstream `Supabase Query.params.since = $.value`. | Manual Trigger -> KV Read, KV Read -> Supabase Query. | KV Read -> Embedding without text/documents. | Good for checkpoints. |
| KV Write | Write workflow KV. | any. | json written. | `key`. | `valuePath`, `ttlSeconds`. | `key: "digest:lastSuccessfulRun"`, `valuePath: "$.completedAt"`, `ttlSeconds: 604800`; output `{ written: true }`; downstream `Logger.message = $.written`. | Stop / Return -> KV Write, KV Write -> Logger. | KV Write -> Image Upscale without image. | Keep TTL explicit for cache-like values. |
| File Reader | Read a file reference. | any URL/object ref. | file. | none. | `source`, `bucket`, `objectKey`, `allowedDomains`. | `source: "object-storage"`, `bucket: "workflow-inputs"`, `objectKey: "$.fileKey"`; output `{ file: { url: "https://storage.example.com/report.pdf", mimeType: "application/pdf" } }`; downstream `Media Convert.file = $.file`. | Object Storage Download -> File Reader, File Reader -> Text Splitter. | File Reader -> Email Send without attachment mapping/body. | Enforce domain allowlist and size limits. |
| File Writer | Write content to a file destination. | text/json/file. | file. | `destination`, `objectKeyTemplate`. | `bucket`, `contentPath`, `mimeType`. | `destination: "object-storage"`, `bucket: "workflow-outputs"`, `objectKeyTemplate: "digests/{{runId}}.md"`, `contentPath: "$.summary"`, `mimeType: "text/markdown"`; output `{ file: { url: "https://storage.example.com/digests/run_1.md", mimeType: "text/markdown" } }`; downstream `Email Send.attachments = $.file`. | Summarizer -> File Writer, File Writer -> Email Send. | File Writer -> Speech To Text unless file is audio. | New runner emits a durable write request for worker/server fulfilment. |
| Object Storage Upload | Upload a file. | file. | file. | `bucket`. | `objectKeyTemplate`, metadata. | `bucket: "workflow-artifacts"`, `objectKeyTemplate: "media/{{runId}}/{{filename}}"`, `filePath: "$.file.url"`; output `{ file: { url: "https://storage.example.com/media/run_1/image.png" } }`; downstream `Slack Notify.text = $.file.url`. | Text To Image -> Object Storage Upload, Upload -> Slack Notify. | Upload -> Output Parser without text. | Server/worker handles bytes. |
| Object Storage Download | Download a file. | json object key. | file. | `bucket`. | `objectKey`. | `bucket: "workflow-artifacts"`, `objectKey: "$.objectKey"`; output `{ file: { url: "blob:downloaded", mimeType: "image/png" } }`; downstream `Image Analyze.image = $.file`. | Manual Trigger -> Object Storage Download, Download -> Image Analyze. | Download -> Embedding unless text is extracted. | Validate bucket permissions. |
| Webhook Send | Send outbound webhook. | json. | json status/response. | `url`. | `method`, headers, body mapping, retry. | `url: "https://hooks.example.com/lenserfight"`, `method: "POST"`, `bodyPath: "$"`; output `{ status: 202, responseBody: { accepted: true } }`; downstream `Logger.message = $.status`. | Data Mapper -> Webhook Send, Webhook Send -> Logger. | Webhook Send -> Image Upscale without image. | Prevent SSRF/private URLs. |
| HTTP Request | Call an HTTP endpoint. | any. | json response. | `url`. | `method`, headers, body, timeout. | `url: "https://api.github.com/repos/org/repo/pulls/42"`, `method: "GET"`, `headers: { Authorization: "Bearer {{secrets.github}}" }`; output `{ status: 200, body: { title: "Add catalog" } }`; downstream `GitHub PR Review.input = $.body`. | Webhook Trigger -> HTTP Request, HTTP Request -> Data Mapper. | HTTP Request -> Speech To Text without audio. | Use Secret Resolver for credentials. |
| GraphQL Request | Call GraphQL endpoint. | json variables. | json data. | `query`. | `endpoint`, variables, headers. | `endpoint: "https://api.github.com/graphql"`, `query: "query($owner:String!){repository(owner:$owner,name:\"lenserfight-web\"){name}}"`, `variables: { owner: "ofcskn" }`; output `{ data: { repository: { name: "lenserfight-web" } } }`; downstream `Summarizer.content = $.data`. | Manual Trigger -> GraphQL Request, GraphQL Request -> Summarizer. | GraphQL Request -> Image Analyze without image. | Same network safety rules as HTTP Request. |

## Communication 🔴

| Node | Purpose | Inputs | Outputs | Required Configuration | Optional Configuration | Example Configuration | Common Valid Connections | Common Invalid Connections | Execution Notes |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Email Send | Send an email with templates and attachments. | text/json/file. | json delivery status/message id. | `to`, `subject`, `body`. | `fromProfile`, `attachments`, `provider`, retry, `onParentFailure`. | `fromProfile: "founder-updates"`, `to: "{{workspace.owner.email}}"`, `subject: "LenserFight weekly AI digest"`, `body: "{{$.summary}}"`, `attachments: [{ name: "leaderboard.csv", filePath: "$.leaderboardFile.url" }]`, `provider: "resend"`, `retry: { attempts: 3, backoffMs: 2000 }`; output `{ status: "sent", messageId: "resend_abc123" }`; downstream `Logger.message = $.messageId`. | Summarizer -> Email Send, File Writer -> Email Send. | Embedding -> Email Send. | Worker/server delivery provider; direct vector output is invalid. |
| Slack Notify | Send Slack message. | any mapped text/json. | json message status. | `channel` or `webhookUrl`, `text`. | provider profile, retry. | `channel: "#arena-alerts"`, `text: "Battle {{$.battleId}} winner: {{$.winner}}"`; output `{ status: "sent", ts: "1715850000.000100" }`; downstream `Logger.message = $.ts`. | Error Catch -> Slack Notify, Leaderboard Update -> Slack Notify. | Embedding -> Slack Notify without mapping. | Use templates for structured payloads. |
| Discord Notify | Send Discord webhook message. | any. | json delivery. | `webhookUrl`. | `content`, embeds. | `webhookUrl: "{{secrets.discordArenaWebhook}}"`, `content: "New battle result: {{$.winner}}"`; output `{ status: "sent", messageId: "discord_123" }`; downstream `Logger.message = $.messageId`. | Battle Execute -> Discord Notify. | Vector Search -> Discord Notify without summary. | Server/worker external call. |
| Telegram Notify | Send Telegram chat message. | any. | json delivery. | `chatId`. | parse mode, disable preview. | `chatId: "{{secrets.telegramOpsChat}}"`, `text: "Workflow failed: {{$.error.message}}"`; output `{ status: "sent", messageId: "tg_123" }`; downstream `Logger.message = $.messageId`. | Error Catch -> Telegram Notify. | File Reader -> Telegram Notify without text. | Keep secrets server-side. |
| Push Notification | Send in-app/device push. | any. | json queued notification. | `audience`. | title, body, deep link. | `audience: "workspace_admins"`, `title: "Digest ready"`, `body: "{{$.summaryTitle}}"`; output `{ status: "queued", notificationId: "push_123" }`; downstream `Logger.message = $.notificationId`. | Schedule Trigger -> Push Notification. | Embedding -> Push Notification without mapper. | Rate limit by audience. |
| SMS Send | Send SMS alert. | any. | json delivery. | `to`, `body`. | provider, country rules. | `to: "{{workspace.owner.phone}}"`, `body: "Critical workflow failed: {{$.workflowName}}"`; output `{ status: "sent", messageId: "sms_123" }`; downstream `Logger.message = $.messageId`. | Error Catch -> SMS Send. | Document[] -> SMS Send without summary. | Server-only; use sparingly due cost/compliance. |

## Integrations 🔴

| Node | Purpose | Inputs | Outputs | Required Configuration | Optional Configuration | Example Configuration | Common Valid Connections | Common Invalid Connections | Execution Notes |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| GitHub Read | Read repo/PR/issue/file data. | json request. | json GitHub data. | `repository`. | resource, number, credentials. | `repository: "ofcskn/lenserfight-web"`, `resource: "pull_request"`, `prNumber: 42`; output `{ pullRequest: { title: "Add workflow catalog", files: 12 } }`; downstream `GitHub PR Review.input = $.pullRequest`. | Webhook Trigger -> GitHub Read, GitHub Read -> Agent Execute. | GitHub Read -> Image Upscale without image. | Use GitHub token secret. |
| GitHub PR Review | Draft or submit PR review. | json review payload. | json review status. | `repository`, `prNumber`, `reviewBody`. | event, comments, credentials. | `repository: "ofcskn/lenserfight-web"`, `prNumber: "$.pullRequest.number"`, `reviewBody: "$.summary"`, `event: "COMMENT"`; output `{ status: "submitted", reviewId: "PRR_kwDO" }`; downstream `Slack Notify.text = $.status`. | Agent Execute -> GitHub PR Review. | Embedding -> GitHub PR Review without review body. | Human approval recommended for requested-changes events. |
| GitHub Issue Create | Create GitHub issue. | json issue data. | json issue id/url. | `repository`, `title`, `body`. | labels, assignees. | `repository: "ofcskn/lenserfight-web"`, `title: "Workflow catalog validation failure"`, `body: "$.reasoning"`, `labels: ["workflow","automation"]`; output `{ issueNumber: 128, url: "https://github.com/ofcskn/lenserfight-web/issues/128" }`; downstream `Slack Notify.text = $.url`. | Judge / Eval -> GitHub Issue Create. | Audio -> GitHub Issue Create without transcription. | Server-side credentials. |
| RSS Feed | Fetch RSS items. | any. | json items. | `feedUrl`. | `limit`, `since`. | `feedUrl: "https://github.blog/feed/"`, `limit: 10`, `since: "$.lastSuccessfulRun"`; output `{ items: [{ title: "Actions update", link: "https://github.blog/..." }] }`; downstream `Summarizer.documents = $.items`. | Schedule Trigger -> RSS Feed, RSS Feed -> Deduplicate. | RSS Feed -> Email Send without summary. | Cap item count before AI. |
| Notion Read | Read Notion pages/database rows. | json. | json results. | `databaseId` or `pageId`. | filter, credentials. | `databaseId: "{{secrets.notionDigestDb}}"`, `filter: { property: "Status", equals: "Ready" }`; output `{ results: [{ title: "Arena notes" }] }`; downstream `Summarizer.content = $.results`. | Schedule Trigger -> Notion Read. | Notion Read -> Image Analyze without image URL. | Credentials server-side. |
| Notion Write | Write page/database row. | json. | json page id/url. | `databaseId`, properties. | parent page, icon. | `databaseId: "{{secrets.notionDigestDb}}"`, `properties: { Name: "$.title", Summary: "$.summary" }`; output `{ pageId: "notion_page_123", url: "https://notion.so/..." }`; downstream `Slack Notify.text = $.url`. | Summarizer -> Notion Write. | Embedding -> Notion Write without mapped properties. | Preserve Notion property types. |
| Sheets Read | Read Google Sheet rows. | json. | array rows. | `spreadsheetId`, `sheetName` or `range`. | value render option. | `spreadsheetId: "{{secrets.weeklyMetricsSheet}}"`, `sheetName: "Battle Metrics"`, `range: "A2:G200"`, `valueRenderOption: "FORMATTED_VALUE"`; output `{ rows: [{ battleId: "battle_123", score: 91 }] }`; downstream `Aggregate.items = $.rows`. | Schedule Trigger -> Sheets Read, Sheets Read -> Aggregate. | Sheets Read -> Speech To Text. | Google credentials required. |
| Sheets Write | Append/update Google Sheet rows. | json rows. | json update status. | `spreadsheetId`, `sheetName`, operation. | `rowsPath`, range. | `spreadsheetId: "{{secrets.weeklyMetricsSheet}}"`, `sheetName: "Digest Log"`, `operation: "append"`, `rowsPath: "$.rows"`; output `{ updatedRows: 3, spreadsheetId: "sheet_123" }`; downstream `Logger.message = $.updatedRows`. | Data Mapper -> Sheets Write. | Embedding -> Sheets Write without row mapping. | Normalize rows before write. |
| Calendar Create | Create calendar event. | json event. | json event id/link. | `calendarId`, title, start, end. | attendees, description. | `calendarId: "primary"`, `title: "Arena digest review"`, `start: "2026-05-18T10:00:00+03:00"`, `end: "2026-05-18T10:30:00+03:00"`, `attendees: ["founder@example.com"]`; output `{ eventId: "cal_123", htmlLink: "https://calendar.google.com/..." }`; downstream `Slack Notify.text = $.htmlLink`. | If -> Calendar Create. | Calendar Create -> Embedding without text. | Timezone must be explicit. |
| Linear Issue Create | Create Linear issue. | json. | json issue id/url. | `teamId`, `title`. | priority, labels. | `teamId: "{{secrets.linearTeamId}}"`, `title: "Investigate low-confidence judge result"`, `description: "$.reasoning"`, `priority: 2`; output `{ issueId: "LIN-321", url: "https://linear.app/..." }`; downstream `Slack Notify.text = $.url`. | Judge / Eval -> Linear Issue Create. | Image -> Linear Issue Create without analysis text. | Credentials server-side. |
| Jira Issue Create | Create Jira issue. | json. | json issue key/url. | `projectKey`, `issueType`, `summary`. | labels, assignee. | `projectKey: "LF"`, `issueType: "Task"`, `summary: "Workflow validation warning"`, `description: "$.warning"`; output `{ issueKey: "LF-42", url: "https://jira.example.com/browse/LF-42" }`; downstream `Slack Notify.text = $.url`. | Error Catch -> Jira Issue Create. | Embedding -> Jira Issue Create without mapper. | Server-side credentials and project permissions. |

## Media Generation 🔴

| Node | Purpose | Inputs | Outputs | Required Configuration | Optional Configuration | Example Configuration | Common Valid Connections | Common Invalid Connections | Execution Notes |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Text to Image | Generate image from prompt. | text. | image. | `model`. | provider, size, funding. | `provider: "fal-ai"`, `model: "fal-ai/flux/dev"`, `promptPath: "$.prompt"`, `size: "1024x1024"`; output `{ image: { url: "blob:image", mimeType: "image/png" } }`; downstream `Object Storage Upload.file = $.image`. | Prompt Template -> Text to Image. | Embedding -> Text to Image without prompt. | Provider-funded media execution. |
| Image to Image | Transform an image. | image plus text prompt. | image. | `model`. | provider, strength. | `provider: "fal-ai"`, `model: "fal-ai/flux-pro/kontext"`, `imagePath: "$.image.url"`, `prompt: "Create a polished arena card"`; output `{ image: { url: "blob:edited", mimeType: "image/png" } }`; downstream `Object Storage Upload.file = $.image`. | File Reader -> Image to Image. | Text Splitter -> Image to Image without image. | Needs durable source image. |
| Image to Audio | Generate audio from image/prompt context. | image. | audio. | `model`. | prompt, duration. | `provider: "fal-ai"`, `model: "fal-ai/stable-audio"`, `imagePath: "$.image.url"`, `prompt: "Ambient intro for the battle replay"`; output `{ audio: { url: "blob:audio", mimeType: "audio/mpeg" } }`; downstream `Object Storage Upload.file = $.audio`. | Image Analyze -> Image to Audio with image passthrough. | RAG Retriever -> Image to Audio without image. | Provider-funded. |
| Text to Speech | Generate speech audio. | text. | audio. | `voice`. | provider, model, format. | `provider: "openai"`, `model: "gpt-4o-mini-tts"`, `voice: "alloy"`, `textPath: "$.summary"`; output `{ audio: { url: "blob:tts", mimeType: "audio/mpeg" } }`; downstream `Object Storage Upload.file = $.audio`. | Summarizer -> Text to Speech. | Embedding -> Text to Speech without text. | Media artifact persisted after execution. |
| Speech to Text | Transcribe speech. | audio. | text. | `model`. | language, timestamps. | `provider: "openai"`, `model: "gpt-4o-transcribe"`, `audioPath: "$.audio.url"`; output `{ text: "The battle winner is..." }`; downstream `Summarizer.content = $.text`. | File Reader -> Speech to Text. | Text to Image -> Speech to Text unless output audio exists. | Use Audio Transcribe AI primitive for richer analysis. |
| Text to Video | Generate video from prompt. | text. | video. | `model`. | duration, aspect ratio. | `provider: "fal-ai"`, `model: "fal-ai/veo3"`, `promptPath: "$.prompt"`, `durationSeconds: 8`, `aspectRatio: "16:9"`; output `{ video: { url: "blob:video", mimeType: "video/mp4" } }`; downstream `Object Storage Upload.file = $.video`. | Prompt Template -> Text to Video. | Embedding -> Text to Video without prompt. | Worker/server for long jobs. |
| Image Upscale | Upscale an image. | image. | image. | `scale`. | provider, model. | `provider: "fal-ai"`, `model: "fal-ai/esrgan"`, `imagePath: "$.image.url"`, `scale: 2`; output `{ image: { url: "blob:upscaled", mimeType: "image/png" } }`; downstream `Object Storage Upload.file = $.image`. | Text to Image -> Image Upscale. | Email Send -> Image Upscale. | Requires image input. |
| Media Convert | Convert media/file format. | file. | file. | `targetFormat`. | bitrate, codec. | `targetFormat: "mp3"`, `inputPath: "$.file.url"`, `audioBitrate: "128k"`; output `{ file: { url: "blob:converted", mimeType: "audio/mpeg" } }`; downstream `Object Storage Upload.file = $.file`. | File Reader -> Media Convert. | RAG Retriever -> Media Convert without file. | Worker/server byte processing. |

## Utility 🔴

| Node | Purpose | Inputs | Outputs | Required Configuration | Optional Configuration | Example Configuration | Common Valid Connections | Common Invalid Connections | Execution Notes |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Logger | Write structured logs. | any. | json logged status. | `message`. | `level`, `includeInput`. | `level: "info"`, `message: "Digest delivered: {{$.messageId}}"`, `includeInput: true`; output `{ logged: true, level: "info" }`; downstream `No-Op.input = $`. | Any -> Logger. | Logger -> Image Upscale without image. | Safe terminal debug node. |
| Debug Inspector | Inspect payload shape. | any. | json inspection. | `capturePaths`. | `redactSecrets`. | `capturePaths: ["$", "$.documents[0].metadata"]`, `redactSecrets: true`; output `{ inspection: { keys: ["documents","summary"] } }`; downstream `No-Op.input = $`. | Any -> Debug Inspector. | Debug Inspector -> Email Send in production without mapper. | Manual/debug surface; redact secrets. |
| Secret Resolver | Resolve a secret reference. | any. | json secret alias. | `secretRef`. | `exposeAs`. | `secretRef: "github-api-token"`, `exposeAs: "githubToken"`; output `{ githubToken: "{{resolved-secret-ref}}" }`; downstream `HTTP Request.headers.Authorization = $.githubToken`. | Manual Trigger -> Secret Resolver, Secret Resolver -> HTTP Request. | Secret Resolver -> Logger with raw secret. | Server-only; never expose raw values to client logs. |
| Rate Limit | Throttle by key/window. | any. | json allowed/remaining. | `limit`. | `key`, `windowSeconds`. | `key: "github-api"`, `limit: 30`, `windowSeconds: 60`; output `{ allowed: true, remaining: 29 }`; downstream `HTTP Request.input = $`. | Split In Batches -> Rate Limit, Rate Limit -> HTTP Request. | Rate Limit -> Speech To Text without audio. | Use before paid/external APIs. |
| Cache Read | Read cached data. | any. | json hit/value. | `key`. | namespace. | `key: "rss:github-blog:last-summary"`; output `{ hit: true, value: { summary: "..." } }`; downstream `If / Condition.condition = $.hit`. | Schedule Trigger -> Cache Read, Cache Read -> If. | Cache Read -> Image Analyze without image. | Use with Cache Write for RSS digests. |
| Cache Write | Write cached data. | any. | json written. | `key`. | `valuePath`, `ttlSeconds`. | `key: "rss:github-blog:last-summary"`, `valuePath: "$.summary"`, `ttlSeconds: 3600`; output `{ written: true }`; downstream `Logger.message = $.written`. | Summarizer -> Cache Write. | Cache Write -> Embedding without text. | Keep TTL intentional. |
| Retry | Retry a branch. | any. | json attempts/succeeded. | `attempts`. | `backoffMs`, `retryOn`. | `attempts: 3`, `backoffMs: 2000`, `retryOn: ["timeout","rate_limit"]`; output `{ attempts: 2, succeeded: true }`; downstream `Logger.message = $.succeeded`. | HTTP Request -> Retry, Retry -> Logger. | Retry -> Image Upscale without image. | Prefer node-level retry when possible. |
| No-Op | Pass input through unchanged. | any. | any. | `label`. | enabled. | `label: "placeholder while designing PR review workflow"`; output `{ output: "unchanged" }`; downstream `Logger.message = $.output`. | Any -> No-Op. | No-Op as production terminal for required side effects. | Useful while designing templates. |

</div>
