Borg Live Debug
karanhudia/borg-ui
Live Borg debugging by exec-ing into the borg-web-ui Docker container.
End-to-end runbook for adding or modifying a Flowfile node type across all four layers (flowfilecore settings/graph/template, flowfilefrontend UI registry, flowfileframe Python API, flowfilewasm…
$ npx skills add Edwardvaneechoud/Flowfile --skill flowfile-node-development -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install Edwardvaneechoud/Flowfile flowfile-node-development --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/Edwardvaneechoud/Flowfile.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.claude/skills/flowfile-node-development .claude/skills/flowfile-node-development && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "flowfile-node-development" agent skill from https://github.com/Edwardvaneechoud/Flowfile/tree/main/.claude/skills/flowfile-node-development into .claude/skills/flowfile-node-development/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "flowfile-node-development", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/Edwardvaneechoud/Flowfile/tree/main/.claude/skills/flowfile-node-developmentType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add Edwardvaneechoud/Flowfile --skill flowfile-node-development -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install Edwardvaneechoud/Flowfile flowfile-node-development --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/Edwardvaneechoud/Flowfile.git skills-src && mkdir -p .agents/skills && cp -r skills-src/.claude/skills/flowfile-node-development .agents/skills/flowfile-node-development && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "flowfile-node-development" agent skill from https://github.com/Edwardvaneechoud/Flowfile/tree/main/.claude/skills/flowfile-node-development into .agents/skills/flowfile-node-development/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "flowfile-node-development", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add Edwardvaneechoud/Flowfile --skill flowfile-node-development -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install Edwardvaneechoud/Flowfile flowfile-node-development --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/Edwardvaneechoud/Flowfile.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/.claude/skills/flowfile-node-development .cursor/skills/flowfile-node-development && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "flowfile-node-development" agent skill from https://github.com/Edwardvaneechoud/Flowfile/tree/main/.claude/skills/flowfile-node-development into .cursor/skills/flowfile-node-development/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "flowfile-node-development", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/Edwardvaneechoud/Flowfile.git --path .claude/skills/flowfile-node-development--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add Edwardvaneechoud/Flowfile --skill flowfile-node-development -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install Edwardvaneechoud/Flowfile flowfile-node-development --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/Edwardvaneechoud/Flowfile.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/.claude/skills/flowfile-node-development .gemini/skills/flowfile-node-development && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "flowfile-node-development" agent skill from https://github.com/Edwardvaneechoud/Flowfile/tree/main/.claude/skills/flowfile-node-development into .gemini/skills/flowfile-node-development/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "flowfile-node-development", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install Edwardvaneechoud/Flowfile flowfile-node-developmentInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add Edwardvaneechoud/Flowfile --skill flowfile-node-development -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/Edwardvaneechoud/Flowfile.git skills-src && mkdir -p .github/skills && cp -r skills-src/.claude/skills/flowfile-node-development .github/skills/flowfile-node-development && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "flowfile-node-development" agent skill from https://github.com/Edwardvaneechoud/Flowfile/tree/main/.claude/skills/flowfile-node-development into .github/skills/flowfile-node-development/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "flowfile-node-development", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add Edwardvaneechoud/Flowfile --skill flowfile-node-development -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install Edwardvaneechoud/Flowfile flowfile-node-development --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/Edwardvaneechoud/Flowfile.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/.claude/skills/flowfile-node-development .opencode/skills/flowfile-node-development && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "flowfile-node-development" agent skill from https://github.com/Edwardvaneechoud/Flowfile/tree/main/.claude/skills/flowfile-node-development into .opencode/skills/flowfile-node-development/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "flowfile-node-development", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
flowfile-node-developmentEnd-to-end runbook for adding or modifying a Flowfile node type across all four layers (flowfilecore settings/graph/template, flowfilefrontend UI registry, flowfileframe Python API, flowfilewasm…
Flowfile Node Development is an agent skill from Edwardvaneechoud/Flowfile. End-to-end runbook for adding or modifying a Flowfile node type across all four layers (flowfilecore settings/graph/template, flowfilefrontend UI registry, flowfileframe Python API, flowfilewasm parity) including the string-convention dispatch trap, the worker-does-all-fetching rule for network sources, and the stub/parity gates each layer enforces. Use when a task says "add a node", "new node type", "add a transform/reader/writer node", "wire up a node in the UI", "expose this as a FlowFrame method", "add this…
Its SKILL.md is about 9.3k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.
It sits in DevOps & Cloud, covering Frontend development, Failing and flaky tests and Runbooks and postmortems. It works with WebAssembly and Python. The repository describes itself as: Flowfile is a visual ETL tool and Python library combining drag-and-drop workflows with Polars dataframes. Build data pipelines visually, define flows programmatically with a… The licence is MIT.
8 steps, taken from the step headings in SKILL.md.
Read from SKILL.md and the folder at commit c054c90. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Shell commands in SKILL.md call:
makenpmpoetryFrom the folder's file list and the shell code blocks in SKILL.md.
No URLs in SKILL.md. Its commands use npm, which can reach the network depending on how they are called.
From URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Flowfile Node Development loads about 9.3k tokens when it runs. Until then it costs about 189 tokens; SKILL.md has 3,743 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from Edwardvaneechoud/Flowfile at commit c054c90, republished under its MIT licence (© Edwardvaneechoud). 3,743 words, ~9,275 tokens.
.claude/skills/flowfile-node-development/SKILL.md (or your agent's skills folder).flow_graph.py execution/caching/scheduling internals without adding a node type → flowfile-architecture-contract (DAG engine, executor decision table, hash/cache invariants).flowfile-debugging-playbook first, then come back here once you know it's a node-registration problem.flowfile-frontend-conventions.flowfile_frame's expression system (Expr, _repr_str, _ff_repr, selectors) beyond "should this be a native node or polars-code" → flowfile-frame-and-codegen.flowfile-codegen-parity-campaign.flowfile-testing-and-validation.FLOWFILE_OFFLOAD_TO_WORKER, FEATURE_FLAG_AI, etc.) → flowfile-config-and-flags.All facts below verified in-repo as of 2026-07-03 (v0.12.7), branch feature/claude-skills. Re-verification commands are in the final section — run them before trusting a line number in a stale checkout.
A "node" is not one artifact. Adding my_node (fictional example used throughout) touches up to four independent registries that only agree by naming convention, not by a shared enum or generated code:
flowfile_core settings schema (input_schema.py) + add_<type> method on FlowGraph
+ NodeTemplate (configs/node_store/nodes.py) + NODE_TYPE_TO_SETTINGS_CLASS
│ served over HTTP as GET /node_list + POST /update_settings/?node_type=...
▼
flowfile_frontend settings Vue component at a convention path, resolved by import.meta.glob
(main desktop/web app only — NOT flowfile_wasm)
flowfile_frame optional: a FlowFrame/Expr method that emits either a native add_<type> node
or a generated polars-code string
flowfile_wasm optional, only if the node must run standalone-in-browser: Python engine fn +
TS settings component + palette entry (own registries, no HTTP, no flowfile_core)Nothing here is enforced by a compiler or a single source of truth. Grep is the index. The canonical command to find every place a node type is wired:
grep -n "def add_" flowfile_core/flowfile_core/flowfile/flow_graph.py # core graph methods
grep -n '"<node_type>"' flowfile_core/flowfile_core/configs/node_store/nodes.py # template
grep -n "<node_type>" flowfile_core/flowfile_core/schemas/schemas.py # settings-class registry
find flowfile_frontend/src/renderer/app/components/nodes/node-types/elements -iname '*<TitleCase>*'Adding a node from the UI/API goes through POST /update_settings/?node_type=<node_type> → add_generic_settings (flowfile_core/flowfile_core/routes/routes.py::add_generic_settings). It resolves everything by string manipulation, at request time, with no import-time check:
# routes.py::add_generic_settings (verified)
input_data["user_id"] = current_user.id
node_type = camel_case_to_snake_case(node_type) # e.g. "fuzzyMatch" -> "fuzzy_match"
add_func = getattr(flow, "add_" + node_type) # AttributeError if no add_fuzzy_match method
setting_name_ref = "node" + node_type.replace("_", "") # "fuzzy_match" -> "nodefuzzymatch"
...
ref = get_node_model(setting_name_ref) # scans input_schema.__dict__ for a
# class whose *lowercased* name matches
...
try:
add_func(parsed_input)
except Exception as e:
raise HTTPException(419, str(f"error: {e}")) from e # non-standard status code, verifiedget_node_model (routes.py) literally does for ref_name, ref in inspect.getmodule(input_schema).__dict__.items(): if ref_name.lower() == setting_name_ref: return ref.
Consequences you must design around:
Node + the node_type with underscores stripped, case-insensitively: text_to_rows → class NodeTextToRows (lowercases to nodetexttorows, matches "node" + "texttorows"). Get one letter wrong and get_node_model silently returns None → the route responds 404 "could not find the interface".add_<node_type> exactly (snake_case, matching what the frontend sends as node_type). Get it wrong and you get AttributeError inside add_generic_settings, which is not caught — it propagates as an unhandled 500, or if it happens after add_func is fetched but the call itself raises inside your method, you get the 419 above.getattr/__dict__ scan.flowfile_core/flowfile_core/schemas/input_schema.py. Subclass one of:NodeSingleInput (input_schema.py:473) — adds depending_on_id: int | None = -1. Use for one-input transforms (filter, sort, select, formula...).NodeMultiInput (input_schema.py:479) — adds depending_on_ids: list[int]. Use for join/union/concat-style nodes.NodeBase (input_schema.py:428) directly — sources with no input (read, manual_input, database_reader...). NodeBase already carries flow_id, node_id, cache_results, pos_x/pos_y, description, node_reference, user_id, is_setup, output_field_config.
Implement get_default_description(); add to_yaml_dict() only if you need curated YAML on save.NODE_TYPE_TO_SETTINGS_CLASS — flowfile_core/flowfile_core/schemas/schemas.py:27. It's a flat dict, "<node_type>": input_schema.Node<CamelCase>. Skip this and saved flows fail to reload with "Unknown node type" (used by NodeInformation.validate_setting_input when deserializing).flowfile_core/flowfile_core/configs/node_store/nodes.py::get_all_standard_nodes() — a NodeTemplate(...) entry in the big list. Required fields: item (== node_type, snake_case), input/output counts, node_type ("input"|"process"|"output"), transform_type ("narrow" → eligible for local-with-sampling execution; "wide" → forced remote/worker under optimize_for_downstream), laziness ("lazy"|"eager"|"conditional"), node_group (palette section), image (an <name>.svg filename), drawer_title/drawer_intro, tags (palette search). Add an entry to the node_defaults dict (nodes.py:755) only if the node should get pre-filled default settings on drop (checked via check_if_has_default_setting at add time, routes.py:594).add_<node_type> method on FlowGraph (flowfile_core/flowfile_core/flowfile/flow_graph.py), decorated @with_history_capture(HistoryActionType.UPDATE_SETTINGS), which runs it inside FlowGraph.transaction (a standalone call records one undo step; inside an editor route, apply_operations or an AI batch it records nothing, because the outer transaction records the gesture), calling the one true factory self.add_node_step(node_id=..., function=_func, node_type="<node_type>", setting_input=..., input_node_ids=[...], schema_callback=...) (add_node_step itself lives at flow_graph.py:3378). _func receives one FlowDataEngine per input slot and returns a FlowDataEngine (or a NamedOutputs for multi-output nodes like filter_split)._func must never write to setting_input. Runs and reads never write settings (TestReadsAndRunsDoNotWrite in tests/flowfile/test_history_invariants.py); reconcile on a per-call deepcopy as add_join/add_select do. Anything the drawer should see derived from the inputs belongs in a @setting_updator_method (flowfile/setting_generator/settings.py), which works on a deep copy._func must tolerate empty schema-only frames. Schema prediction runs your _func against FlowDataEngine.create_from_schema(...) placeholder inputs so the UI can show output columns without executing anything (flow_node.py:1060, _predicted_data_getter — surfaced via get_predicted_resulting_data). Don't assume real row data inside _func unless you gate on it.add_node_step and build/patch the FlowNode directly, registering via self.add_node_to_starting_list(node) + self._node_db[...] — see add_read (flow_graph.py:4646) as the template if you're building a source with special schema-callback logic._func — do not fetch from core.flowfile_core/flowfile_core/flowfile/code_generator/, may need a new handler for Export-to-Python), the flowfile_frame API method (§3, optional), the WASM node set (§4, optional — the runnable palette + available:false teaser entries in flowfile_wasm/src/config/nodeCatalog.ts), the AI tools' node catalog (context for the AI agents to know the node exists — search flowfile_core/flowfile_core/ai/ for existing node references if the node should be AI-discoverable).flowfile_core/tests/flowfile/ (e.g. test_basic_filter.py, test_filter_expressions.py are the pattern for a simple transform node). Assert: settings validate, the graph method adds a node with the right template counts, .collect()/execution produces correct data, and (if the node has a native vs polars-code split anywhere downstream) round-trip through save/reload.filterRead this to see the whole chain end to end for the simplest realistic case.
input_schema.NodeFilter(NodeSingleInput) (input_schema.py:542) → holds filter_input: transform_schema.FilterInput plus split_mode: bool for the pass/fail two-output variant.NODE_TYPE_TO_SETTINGS_CLASS["filter"] = input_schema.NodeFilter (schemas.py:27 block).NodeTemplate(name="Filter data", item="filter", input=1, output=1, transform_type="narrow", node_type="process", laziness="lazy", image="filter.svg", ...) (configs/node_store/nodes.py, in the list built by get_all_standard_nodes).add_filter (flow_graph.py:2224): _func(fl: FlowDataEngine) builds a polars-expr-transformer expression from either the advanced filter string or a BasicFilter locally (a run never writes settings; the drawer's pre-filled advanced expression comes from the display-only filter updator in setting_generator/settings.py), then returns fl.filter_split(expression) in split mode or fl.do_filter(expression) otherwise. Ends with self.add_node_step(..., node_type="filter", renew_schema=False, input_node_ids=[filter_settings.depending_on_id]).POST /update_settings/?node_type=filter → generic add_generic_settings (§1.1) — no filter-specific route exists.read and joinread (source, add_read at flow_graph.py:4646): settings NodeRead(NodeBase) carries a discriminated union InputTableSettings (csv/json/parquet/excel/ipc/ndjson/avro). _func() picks the execution path at run time: local/parquet/ipc/ndjson/utf-8-CSV stay in-core as a lazy scan (FlowDataEngine.create_from_path); exotic encodings and Excel go to the worker to convert to parquet first (FlowDataEngine.create_from_path_worker, an ExternalCreateFetcher). The schema callback is chosen by file type (declared fields win; else read-the-file per format; Excel uses a bounded 100-row sample) and is set on both node.schema_callback and node.user_provided_schema_callback so it survives a reset(). read is also the only node type with source-file change detection — the executor compares file-stat snapshots and reruns automatically if the underlying file changed on disk, and reset() deliberately preserves that tracking state.join (two-input, add_join at flow_graph.py:2537): settings NodeJoin(NodeMultiInput) → join_input: transform_schema.JoinInput. _func(main, right) deep-copies the join input, recomputes per-field is_available before calling main.join(...). Uses an explicit schema callback (calculate_join_schema from flowfile/schema_callbacks.py) instead of running _func on placeholder frames — this predicts the output schema without executing the join. Template: input=2, transform_type="wide" (wide ⇒ eligible to be forced onto the worker when optimizing downstream execution).Rule, verified against three current implementations: a node whose _func talks to the outside world (HTTP API, OAuth provider, Kafka broker, a database over the network) must not perform that I/O inside flowfile_core. Build a worker-settings payload and dispatch it through an External*Fetcher (flowfile_core/flowfile_core/flowfile/flow_data_engine/subprocess_operations/subprocess_operations.py) — the fetcher POSTs the settings to flowfile_worker, which does the actual network round-trip in a spawned subprocess and hands back a cached, lazy result.
Canonical zero-local-branch template — add_google_analytics_reader (flow_graph.py:4254): _func() unconditionally builds ExternalGoogleAnalyticsFetcher(_build_worker_settings(), wait_on_completion=False) and calls .get_result(). There is no execution_location == "local" branch at all — GA4 always round-trips through the worker, full stop. The schema callback is derived purely from the selected metrics/dimensions (derive_schema, pure Python, no network call), so schema prediction never has to touch the worker either.
Default to the GA4 pattern for new network-source nodes. It is the smallest, least error-prone shape: one fetch path, one place secrets get decrypted (worker-side, just-in-time), nothing to keep in sync.
The nuance (verified, corrects a stricter-sounding rule of thumb): two existing readers — add_database_reader (flow_graph.py:3950) and add_rest_api_reader (flow_graph.py:4399) — do carry an execution_location == "local" branch that fetches in-process instead of going through the worker. This exists specifically to support --run-flow/CLI execution and other package/local modes where no worker process exists at all (core forces execution_location="local" and OFFLOAD_TO_WORKER=False in that mode). The REST API reader's local branch was added later than its worker-only fetch path and is explicitly modeled on the database reader (comment in source: # No worker service in local runs — fetch in-process (cf. add_database_reader).) — it is a deliberate, kept pattern, not a one-off legacy wart to avoid copying.
So the actual rule is:
execution_location != "local" — that's the worker's job, no exceptions.if self.execution_location == "local": in-process fetch branch if your node has a concrete need to work without a worker (CLI/--run-flow, scheduled local execution). If you add one, mirror add_rest_api_reader's shape exactly: decrypt/resolve credentials the same way for both branches, and call the same underlying fetch function the worker would use (e.g. shared/rest_api/fetch.py::fetch_rest_api) so the two paths can't silently diverge.add_generic_settings returns when your add_<type> method raises for any reason (routes.py::add_generic_settings). If a frontend network tab shows a bare "419" with no obvious cause, the bug is inside your node's _func/settings validation, not the transport.add_node_step deletes-then-recreates a node whose node_type changed but keeps it (via update_node) if the type is unchanged — don't assume "the node with this id" is stable across settings edits that change the node's own type.input > 2 or multi=True in their template use append-connection semantics; templates with input <= 2 replace the main input on a new connection instead of appending (flow_node.py:689-693) — relevant if your node ever needs more than two ordered inputs.Not required if the node is core-only (e.g. an internal/AI-orchestrated node with no user-facing settings). Required for anything a human drags onto the canvas.
There is no static list of node components. Two independent import.meta.glob calls, which must be kept pointing at the same tree, resolve a settings component purely from the node's item string:
app/composables/useDragAndDrop.ts:88 — import.meta.glob("../components/nodes/node-types/elements/**/*.vue"). getComponent() (useDragAndDrop.ts:158) builds the path elements/${camelCase(item)}/${TitleCase(item)}.vue and loads it for the VueFlow node itself.app/components/nodes/GenericNode.vue:25 — import.meta.glob("./node-types/elements/**/*.vue"). loadDrawerComponent() (GenericNode.vue:47) applies the same convention for the settings-drawer component, via defineAsyncComponent with a 3s timeout and up to 3 retries.TitleCase = each _-separated word of the node_type capitalized and joined: text_to_rows → dir textToRows, file TextToRows.vue. So text_to_rows resolves to exactly:
src/renderer/app/components/nodes/node-types/elements/textToRows/TextToRows.vueIf you get the path wrong, there is no build error. The glob still succeeds (it just doesn't find your file); loadDrawerComponent logs a console.error and the settings drawer silently never opens for that node. This is a pure-runtime failure mode — verify by actually opening the node's settings drawer in a running app, not just by compiling.
item, image, counts must already exist in /node_list.<image>.svg to src/renderer/app/features/designer/assets/icons/.src/renderer/app/components/nodes/node-types/elements/<camelCase(item)>/<TitleCase(item)>.vue. Copy the filter/Filter.vue pattern:<generic-node-settings v-model="nodeXxx" @update:model-value="handleGenericSettingsUpdate" @request-save="saveSettings">.const { saveSettings, pushNodeData, handleGenericSettingsUpdate } = useNodeSettings({ nodeRef, onBeforeSave }) from app/composables/useNodeSettings.ts — it auto-sets is_setup = true and POSTs through nodeStore.updateSettings.loadNodeData(nodeId) (fetch via nodeStore.getNodeData(nodeId, false), hydrate setting_input) and end with defineExpose({ loadNodeData, pushNodeData, saveSettings }) — NodeSettingsDrawer.vue requires both hooks to exist on every drawer-entry component ("Universal Apply" contract).loadNodeData: open-time fix-ups stay in the draft and go out with the next save. Closing the drawer saves only real user edits, except for a node whose NodeData.is_setup was false when the drawer loaded — then it saves what the drawer shows. setting_input alone can't tell: for a promise it may be a generated proposal that looks configured.prod_ready: false on the backend NodeTemplate while iterating — keeps it out of the production palette but it still loads fine in saved flows (useful for landing a half-finished node without exposing it).Drag the node from the palette, open its settings drawer, and confirm it loads — vue-tsc and npm run build:web will not catch a wrong component path. If you don't have a running desktop/web session available, at minimum grep for the exact expected path and confirm the file exists there with that exact casing (case-sensitive on Linux CI even if your local macOS filesystem is case-insensitive and would mask the bug).
Not required for most nodes — many core nodes (kernel/python_script, kafka_source, most ML nodes) have no flowfile_frame method and that's fine. Add one only when the node should be reachable from import flowfile_frame as ff code.
flowfile_frame builds the same in-process FlowGraph the UI edits — every FlowFrame method call is really "mutate a graph, then re-read the resulting LazyFrame." Two paths exist for any new method:
input_schema.NodeXxx(...), call self.flow_graph.add_xxx(settings), return self._create_child_frame(new_node_id). This renders as a first-class editable node if the user opens the graph in the Designer.FlowFrame._add_polars_code, flowfile_frame/flowfile_frame/flow_frame.py:588): for cases with no dedicated node, or too many parameter combinations to model — generates a Polars source string (output_df = input_df.<method>(...)) that core re-executes in its sandboxed PolarsCodeExecutor (flowfile_core/flowfile_core/flowfile/flow_data_engine/polars_code_parser.py) at run time. The generated string is load-bearing — a wrong string breaks graph execution silently, only surfacing when the graph actually runs. Contract: no import statements, no exec/eval/getattr/open, must assign output_df (or be a single-line pl./col(/input_df/expr(-prefixed expression), bare Polars dtype names resolve without a pl. prefix, 1 input → input_df, ≥2 inputs → input_df_1..N.new_node_id = generate_node_id() # utils.py:109 — process-global counter, not per-graph
# decide native vs polars-code (mirror an existing use_polars_code_path / can_use_native flag)
settings = input_schema.NodeXxx(
flow_id=self.flow_graph.flow_id, node_id=new_node_id,
depending_on_id=self.node_id, description=description, is_setup=True,
)
self.flow_graph.add_xxx(settings)
return self._create_child_frame(new_node_id) # flow_frame.py:399 — wires the connection + re-reads dataMulti-input methods (join, concat, fuzzy_join) wire "main"/"right" connections manually instead of using _create_child_frame.
Node ids are process-global, not per-graph — generate_node_id() increments a module-level counter shared across every graph in the process, so ids can skip within a single graph (this is expected, not a bug). Every graph method should accept description: str | None = None — it becomes the frontend node label.
Because FlowFrame and Expr inject most of their methods at runtime (add_expr_methods, @add_lazyframe_methods), static type checkers see nothing without committed .pyi stub files. Any change to the public surface of flow_frame.py or expr.py requires regenerating stubs:
make stubs # regenerates flowfile_frame/flowfile_frame/*.pyi (expr.pyi, flow_frame.pyi, one per module)
make check_stubs # CI drift gate: reruns `stubs`, then `git diff --exit-code` on the .pyi filesBoth commands import flowfile_frame, which imports flowfile_core, which runs Alembic migrations against the live catalog DB on import. Isolate before running either locally:
FLOWFILE_DB_PATH=/tmp/stub_scratch.db make stubsmake check_stubs is wired as the check-stubs job in .github/workflows/test.yaml, triggered on any flowfile_frame/** change — a public-surface change without regenerated stubs fails CI, not just a lint warning. If you changed the passthrough-methods set (methods that delegate straight to the underlying LazyFrame with no node added, e.g. collect, schema, describe), update the duplicated copy of that set inside flow_frame_stub_generator.py too — it's hand-duplicated from lazy_methods.py and won't auto-sync.
description.LazyFrame = DataFrame = FlowFrame aliases and the Polars dtype re-exports in __init__.py intact — generated code depends on them existing.make stubs with an isolated FLOWFILE_DB_PATH, stage the .pyi diff (do not commit it yourself — hand off per flowfile-change-control's no-agent-commit policy)..collect() against a plain-Polars reference) and the graph shape (assert node type / generated code string), mirroring flowfile_frame/tests/test_ff_repr.py's _is_formula_node/_is_polars_code_node helper pattern.FLOWFILE_DB_PATH=/tmp/frame_test.db poetry run pytest flowfile_frame/tests --disable-warnings.Only needed if the node must run standalone in the browser with no flowfile_core at all. WASM is a fully separate, from-scratch implementation — it does not import or call the core node code; every node it supports has its own Python engine function and its own TS settings component. Check first whether the node even belongs there: WASM ships a runnable palette (nodeCatalog.ts is the source of truth: read, manual_input, external_data, read_from_catalog, filter, select, sort, group_by, unique, formula, record_id, dynamic_rename, sample/head, pivot, unpivot, join, cross_join, record_count, union, explore_data, output, external_output, write_to_catalog, polars_code) plus locked/greyed-out teaser entries (database_reader, cloud_storage_reader, rest_api_reader, kafka_source, google_analytics_reader, window_functions, sql_query, python_script, fuzzy_match, graph_solver, explode_hierarchy, gate, train_model, apply_model, evaluate_model, database_writer, cloud_storage_writer) that link out to the full-app docs instead of running. Anything needing a backend connection, secrets, or a Docker kernel is a locked placeholder in WASM by design — don't try to make it runnable there.
Touch, in order:
src/pyodide/engine/nodes_*.py — add an execute_<type>(...) function in the right module by responsibility (nodes_io.py, nodes_transform.py, nodes_combine.py, nodes_aggregate.py, nodes_formula.py, nodes_explore.py, nodes_polars_code.py); re-export it via __init__.py, which has two places that must both list the new name: the from .module import (...) block per submodule, and an explicit __all__ = [...] list further down (grouped by submodule with comments) — the browser runs from engine import *, which honors __all__, so a name missing from __all__ silently never reaches the JS bridge even though the plain Python import works fine under pytest.src/stores/flow-store.ts — add a case in executeNode's dispatch to call the bridge string execute_<type>(...).src/config/nodeCatalog.ts — add the palette entry (Canvas.vue builds nodeCategories from it), then run make wasm_node_manifest (the share-link manifest is generated from this file; make check_wasm_node_manifest gates drift). In src/components/Canvas.vue, add the type to the getSettingsComponent(type) map (explicit map here, unlike the main app's convention-glob — every WASM node is a named case).src/components/nodes/<X>Settings.vue — new settings panel (flat file, not a per-node directory like the main app).src/types/index.ts — add the key to NODE_TYPES.src/config/nodeDescriptions.ts — add the palette description entry.src/composables/useCodeGeneration.ts (if the node needs its own Export-to-Python string) and src/stores/schema-inference.ts (pure-TS schema prediction; return null here for anything needing real execution to know its output schema, e.g. polars_code/formula/pivot/joins-without-a-right-schema — that forces the UI to fall back to lazy execution for schema).Any dependency that ships as a compiled Rust extension for polars (the class of package polars-expr-transformer's optional heavy features, polars_ds, and similar plugin-style crates belong to) generally has no wasm32 wheel, because it's compiled per-target and nobody publishes a browser-Pyodide build. If a node's engine function needs such a dependency:
flowfile_frame treats polars_ds as lazy/optional in the formula-upgrade path, and how the WASM formula node only pulls in the subset of polars-expr-transformer that has no polars_ds dependency).import of such a package to a nodes_*.py module — that breaks import.meta.glob/Pyodide package loading for every node in the file, not just yours, because Pyodide's micropip/loadPackage has nothing to install.flowfile_wasm/tests/python/ runs under real CPython against a pinned Polars version — it will happily import a package that has a CPython wheel but no wasm32 wheel, and pass. The only real gate is the pyodide-smoke job (tests/pyodide-smoke/smoke.cjs, run in CI's flowfile-wasm-build.yml under the pyodide-smoke job): it installs actual Pyodide + the actual browser-target packages and replays the JS→Python bridge calls against them. If you add a new Python dependency to the engine, verify it under pyodide-smoke (or manually via npm run dev at :5174 and executing the node in a real browser) before considering the change done — a green npm run test:run (happy-dom, CPython-backed pytest) is not sufficient evidence.execute_*. Never wire a node's settings load, selection, or drag/drop to run data — regression-guarded by tests/unit/no-auto-run.test.ts..collect() unless required. Only output, pivot, explore_data, polars_code materialize; everything else should stay a lazy Polars plan until the user explicitly runs.grep -n "pyodide.js" flowfile_wasm/src/stores/pyodide-store.ts) as "the last Pyodide release with Polars support" — don't bump it casually.| Layer | Where | Command | Notes |
|---|---|---|---|
| core | flowfile_core/tests/flowfile/ | poetry run pytest flowfile_core/tests | Follow test_basic_filter.py for a single-input transform pattern. |
| frontend | flowfile_frontend/tests/ (Playwright), colocated *.test.ts (Vitest) | npm run test:unit, npm run test:web | No automated check exists for the glob-path convention itself — verify by driving the app (drag, open drawer). |
| flowfile_frame | flowfile_frame/tests/ | FLOWFILE_DB_PATH=<scratch> poetry run pytest flowfile_frame/tests --disable-warnings | Assert both .collect() correctness and graph shape (node type / generated code), like test_ff_repr.py. |
| wasm | flowfile_wasm/tests/{python,unit,integration} + real-Pyodide smoke | npm run test:run (CPython-backed engine tests, happy-dom component tests) + pyodide-smoke job for anything touching Python deps | CPython pytest passing does not prove a wasm32-incompatible dependency is safe — see §4.2. |
| Symptom | Cause | Where to look |
|---|---|---|
POST /update_settings/ → HTTP 419 | Your add_<type> method raised an exception | routes.py::add_generic_settings; add a try/except with a clearer message inside your _func or settings validator |
POST /update_settings/ → 404 "could not find the interface" | get_node_model couldn't match your settings class name | Class name must be exactly "node" + node_type.replace("_","") case-insensitively — §1.1 |
POST /update_settings/ → AttributeError / unhandled 500 | No add_<node_type> method on FlowGraph, or a typo in it | getattr(flow, "add_" + node_type) in routes.py::add_generic_settings — §1.1 |
| Reloading a saved flow raises "Unknown node type" | Forgot NODE_TYPE_TO_SETTINGS_CLASS entry | schemas.py:27 — §1.2 step 2 |
| Node settings drawer never opens, only a browser console error | Settings component path doesn't match elements/<camelCase>/<TitleCase>.vue | §2.1 — check both globs (useDragAndDrop.ts, GenericNode.vue) resolve the same path |
make check_stubs fails in CI | Public flow_frame.py/expr.py surface changed without regenerating .pyi files | §3.3 |
A WASM node works in npm run test:run but not in a real browser | A new Python dependency has no wasm32 wheel, imported eagerly | §4.2 — make the import lazy, verify under pyodide-smoke |
Node ids "skip" inside one flowfile_frame graph | Not a bug — generate_node_id() is a process-global counter | §3.2 |
Facts below are dated 2026-07-03 (v0.12.7), branch feature/claude-skills. Re-run these before trusting a specific line number in a newer checkout — this codebase's node-related files churn fast (flow_graph.py alone is ~6k lines and gains add_* methods regularly).
# core: dispatch trap + settings-class lookup (routes.py) — confirm line numbers/behavior unchanged
grep -n "def get_node_model\|def add_generic_settings" -A 5 flowfile_core/flowfile_core/routes/routes.py
# core: settings-class registry (must include your new node_type)
grep -n "NODE_TYPE_TO_SETTINGS_CLASS" -A 5 flowfile_core/flowfile_core/schemas/schemas.py
# core: index every add_<type> method (grep is the practical index of the node API)
grep -n "def add_" flowfile_core/flowfile_core/flowfile/flow_graph.py
# core: confirm which network-source nodes have a local-execution branch (should stay GA4=none, DB/REST=yes)
grep -n 'execution_location == "local"' flowfile_core/flowfile_core/flowfile/flow_graph.py
# frontend: confirm both node-component globs still point at the same tree
grep -n "import.meta.glob" flowfile_frontend/src/renderer/app/composables/useDragAndDrop.ts \
flowfile_frontend/src/renderer/app/components/nodes/GenericNode.vue
# flowfile_frame: stub pipeline still wired the same way
grep -n "^stubs:\|^check_stubs:" -A 8 Makefile
# flowfile_frame: isolate the DB before running stubs/tests (it imports flowfile_core -> runs Alembic)
FLOWFILE_DB_PATH=/tmp/ff_scratch.db poetry run pytest flowfile_frame/tests --disable-warnings -q
# wasm: current runnable palette + locked placeholders (counts/names drift — root CLAUDE.md is known-stale here)
grep -n "nodeCategories\s*=\|getSettingsComponent" flowfile_wasm/src/components/Canvas.vue
grep -n "pyodide.js\|loadPackage" flowfile_wasm/src/stores/pyodide-store.ts
# wasm: confirm the pyodide-smoke CI gate still exists (the only guard for wasm32-wheel gaps)
grep -n "pyodide-smoke" .github/workflows/flowfile-wasm-build.yml© Edwardvaneechoud, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
Just SKILL.md in .claude/skills/flowfile-node-development of Edwardvaneechoud/Flowfile.
Open the folder on GitHubat commit c054c90
Flowfile Node Development next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Flowfile Node Development this skillEdwardvaneechoud/Flowfile | 370 | — | ~9.3k | Automated safety check: Pass | MIT | |
| Borg Live Debugkaranhudia/borg-ui | 1.7k | — | ~1.4k | Automated safety check: Notes | AGPL-3.0 | |
| RustPython Test Failure InvestigationRustPython/RustPython | 22k | — | ~467 | Automated safety check: Pass | MIT | |
| MCP Debuggerdebugmcp/mcp-debugger | 171 | — | ~3.8k | Automated safety check: Pass | MIT | |
| Dbgtheodo-group/debug-that | 158 | — | ~1.9k | Automated safety check: Pass | MIT | |
| CI/CD Failure Troubleshootingruby-git/ruby-git | 1.8k | — | ~1.9k | Automated safety check: Pass | MIT |
karanhudia/borg-ui
Live Borg debugging by exec-ing into the borg-web-ui Docker container.
RustPython/RustPython
Investigates a failing RustPython test by comparing it with CPython, then either fixes it or gathers the details for an incompatibility report.
debugmcp/mcp-debugger
A skill your agent uses when investigating a bug, failing test, or unexpected runtime behavior and the mcp-debugger MCP server is available — drives real step-through debuggers (breakpoints, stack…
theodo-group/debug-that
Debug applications using the dbg CLI debugger. An agent skill from theodo-group/debug-that.
ruby-git/ruby-git
Diagnoses and fixes failing GitHub Actions runs by identifying the failure, fetching only the relevant logs, finding the root cause and reproducing it locally.
platonai/Browser4
Fix bugs in code using a compile-test-fix loop: run the build/tests, read the error, locate the faulty lines, edit, and re-verify until green.
Edwardvaneechoud/Flowfile
Maps the /ai/ subsystem of flowfile_core, its three agent tiers, litellm seam, BYOK keys and rate limits, and sets rules for extending or debugging it safely.
Edwardvaneechoud/Flowfile
Maps Flowfile's core, worker, frontend, kernel, scheduler and shared services and the design contracts between them, for onboarding and cross-service debugging.
Edwardvaneechoud/Flowfile
Recreates every Flowfile development and build environment from scratch, with exact version pins and an explanation of what each Makefile target really does.
Edwardvaneechoud/Flowfile
Explains how changes to the Flowfile monorepo are gated, versioned and released, including version sync, stub and docs drift checks, Alembic migrations and pinned dependencies.
Edwardvaneechoud/Flowfile
Runbook for closing gaps between a Flowfile visual flow's results and its exported Polars or FlowFrame Python code, measured by tests rather than by eye.
Edwardvaneechoud/Flowfile
Catalog of Flowfile's environment variables and runtime flags: what each does, where the code reads it, its default, and where the docs disagree with the code.
Works with
End-to-end runbook for adding or modifying a Flowfile node type across all four layers (flowfilecore settings/graph/template, flowfilefrontend UI registry, flowfileframe Python API, flowfilewasm…. Flowfile Node Development is an agent skill from Edwardvaneechoud/Flowfile. End-to-end runbook for adding or modifying a Flowfile node type across all four layers (flowfilecore settings/graph/template, flowfilefrontend UI registry, flowfileframe Python API, flowfilewasm parity) including the string-convention dispatch trap, the worker-does-all-fetching rule for network sources, and the stub/parity gates each layer enforces.
Flowfile Node Development fits situations like: A task says add a node; add a transform/reader/writer node; wire up a node in the UI; expose this as a FlowFrame method.
Run `npx skills add Edwardvaneechoud/Flowfile --skill flowfile-node-development -a claude-code`. Or copy the skill folder (.claude/skills/flowfile-node-development in Edwardvaneechoud/Flowfile) into .claude/skills/flowfile-node-development in your project. Claude Code loads it when a task matches its description.
Run `npx skills add Edwardvaneechoud/Flowfile --skill flowfile-node-development -a codex`. Or copy the skill folder (.claude/skills/flowfile-node-development in Edwardvaneechoud/Flowfile) into .agents/skills/flowfile-node-development in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add Edwardvaneechoud/Flowfile --skill flowfile-node-development -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/flowfile-node-development, .gemini/skills/flowfile-node-development, .github/skills/flowfile-node-development and .opencode/skills/flowfile-node-development in your project.
Going by SKILL.md and its folder, Flowfile Node Development needs the command-line tools its instructions call (make, npm and poetry). Our summary lists: Python 3.
SKILL.md contains no URLs. Its commands use npm, which can reach the network depending on how they are called. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Flowfile Node Development is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 9.3k tokens (SKILL.md is roughly 37k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Flowfile Node Development: Borg Live Debug (karanhudia/borg-ui, 1.7k stars), RustPython Test Failure Investigation (RustPython/RustPython, 22k stars), MCP Debugger (debugmcp/mcp-debugger, 171 stars) and Dbg (theodo-group/debug-that, 158 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
Edwardvaneechoud (a GitHub user) maintains it in Edwardvaneechoud/Flowfile, which has 370 GitHub stars. The repository holds 19 skills in this directory. The repository was last updated on October 6, 2026.
Source: Edwardvaneechoud/Flowfile on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.