--- name: flowfile-research-frontier description: The maintainer's three long-horizon research bets for Flowfile (AI-native flow building, visual↔code round-trip, decentralized execution on a centralized catalog) with the verified existing assets each builds on, PR-sized first steps, falsifiable milestones, and the research discipline that turns a hunch into a merged change. Use when scoping ambitious/exploratory work, writing a design doc or spike, proposing a "big idea," choosing what to prototype next, evaluating whether an experiment is worth shipping, or asked "what is Flowfile trying to become" / "where is this going" / "research direction" / "north star." --- # Flowfile Research Frontier This is a **map of unbuilt territory**, not a description of shipped features. It names the maintainer's three standing research bets (as of 2026-07-03, v0.12.7), the concrete repo assets each one can stand on, and the discipline for taking a hunch to a merged change. Everything here labeled *open* or *candidate* is deliberately not built yet — do not describe it as if it ships. Use this skill to **scope and de-risk** exploratory work. It tells you the falsifiable milestone to aim at and the first three PR-sized steps, so a spike produces a measurable result instead of a demo. It does not replace the how-to skills — it points at them. ## When NOT to use this skill - Actually shipping a new node → **flowfile-node-development**. - Changing the AI agent/planner/providers → **flowfile-ai-subsystem** (owns all planner internals). - Fixing a codegen ↔ graph-execution mismatch → **flowfile-codegen-parity-campaign**. - The FlowFrame emission paths / stub pipeline → **flowfile-frame-and-codegen**. - Env vars, feature flags, the `MutableBool` experiment-flag mechanism → **flowfile-config-and-flags**. - How to prove a change is correct (test markers, isolation, the evidence bar) → **flowfile-testing-and-validation**. - Getting a change merged (PRs, drift gates, review) → **flowfile-change-control**. - Why a past idea was abandoned → **flowfile-failure-archaeology**. If your task is to *build* one specific thing, you probably want a sibling above. Come here to decide *what is worth building* and *how you will know it worked*. ## The three-word compass The maintainer's frontier, verbatim intent: - **A — AI-native flow building.** Agents that *reliably* build and edit pipelines, not demos. - **B — Visual ↔ code round-trip.** Full two-way parity between visual flows, exported Python (raw Polars), and the FlowFrame API. - **C — Decentralized execution on a centralized catalog.** Global components, catalog *metadata* in PostgreSQL, table *data* in S3 — and every local Flowfile instance brings its **own** compute, mixing local files and remote tables in complete harmony. Each pillar below follows the same shape: **why the state of the art fails → the specific asset Flowfile already has (verified) → three PR-sized first steps → a falsifiable milestone.** --- ## Pillar A — AI-native flow building ### Why current SOTA fails AI flow-builders are demos. They generate a plausible pipeline once, on a clean canvas, and fall over on the second turn: they hallucinate column names, loop on tool-name mismatches, and mutate a live graph mid-run so a user's concurrent edit silently clobbers the model's work (or vice-versa). There is no *staging*, no *drift detection*, no *atomic accept/reject* — so "reliable editing of an existing pipeline" (the actual job) is unsolved. ### Flowfile's specific existing asset (VERIFIED, as of 2026-07-03 v0.12.7) Flowfile already has the machinery those demos lack — the **diff-staged planner**: - A real multi-turn agent package: `flowfile_core/flowfile_core/ai/agents/planner/` (`loop.py`, `insertion.py`, `recovery.py`, `staged_schemas.py`, …). Not a stub. - Every LLM tool call goes through `execute_tool_call(...)` in `flowfile_core/flowfile_core/ai/tools/executor/dispatch.py:37`, which takes `mode: ExecutionMode = "apply"`. The planner dispatches with **`mode="stage"`** so the **live graph is never mutated mid-run** — steps accumulate as staged entries. - On completion the staged steps bundle into one reviewable `GraphDiff` via `bundle_staged_results(...)` at `flowfile_core/flowfile_core/ai/diff.py:318`, accepted/rejected **atomically** in the UI. - Drift is handled: the session keeps a graph snapshot (`sessions.capture_graph_snapshot`, re-taken on every resume path in `agents/planner/loop.py`); a user canvas edit mid-run emits a `drift_detected` event and parks the session in `paused_drift` status until an explicit resume. - Rejected steps are retried by feeding the executor's `refusal_detail` back as a `role="tool"` message (`ai/tools/executor/refusals.py`), and there is a per-session dry-run cache (`ai/tools/dry_run.py`). So Flowfile is *past* the demo layer. The frontier is **measured reliability** on top of it. (Planner internals are owned by **flowfile-ai-subsystem** — read it before touching this code.) ### First three concrete steps (each PR-sized) 1. **Build a held-out benchmark harness.** Add a fixture corpus of `(natural_language_task, reference_flow)` pairs under `flowfile_core/tests/ai/`, driving `agents/planner/loop.py` end-to-end against a **recorded/replayed** provider (the shared `LiteLLMProvider` seam makes every call interceptable — see flowfile-ai-subsystem). Assert: staged `GraphDiff` applies cleanly **and** the applied flow executes to a frame equal to the reference. Report success rate and retry count. This makes "reliable" a number *before* you touch the agent. 2. **Instrument the refusal path.** Emit structured refusal-reason counters from `ai/tools/executor/refusals.py` across the benchmark, so "the agent is flaky" becomes a ranked table of *which* tool calls fail and *why*. Failure is free — do not make refusals raise. 3. **Add one executor-seam coercion for the top refusal reason**, behind an experiment flag (see flowfile-config-and-flags for the `MutableBool` pattern). Doctrine: when the model consistently emits a recoverable wrong shape, **normalize at the executor seam** (mirror the existing `_coerce_*` handlers in `ai/tools/executor/coercions.py`) — do **not** keep tightening prompts. Measure the benchmark success delta the coercion buys. ### Falsifiable milestone — you have a result when… Over an N-task held-out benchmark, an agent given a task and a blank-or-partial graph produces a staged diff that **(a)** applies cleanly and **(b)** executes to a frame equal to the human reference (`assert_frame_equal`, order-insensitive), at a **measured success rate ≥ your pre-registered threshold**, with **zero human tool-name corrections** and a mean retry count below a pre-registered bound. If the harness cannot report those three numbers, you have a demo, not a result. --- ## Pillar B — visual ↔ code round-trip ### Why current SOTA fails Every visual ETL tool is a one-way street. It might *export* a script, but the export is a lossy, un-reimportable artifact — edit the code and you can never get back to the diagram; edit the diagram and the code silently drifts. There is no tool where the visual graph, hand-written code, and a fluent API are **three renderings of one truth** that survive a round-trip. ### Flowfile's specific existing asset (VERIFIED, as of 2026-07-03 v0.12.7) Flowfile has both directions of the bridge and one canonical DAG behind them: - **Graph → code**: `flowfile_core/flowfile_core/flowfile/code_generator/code_generator.py` exposes `export_flow_to_polars(flow_graph)` (line 1602) and `export_flow_to_flowframe(flow_graph)` (line 1607), via `FlowGraphToPolarsConverter` / `FlowGraphToFlowFrameConverter`. - **Code → graph**: `flowfile_frame` builds the **same** in-process `FlowGraph` as a side effect of every method call — native nodes where a schema exists, generated Polars-code nodes otherwise (`FlowFrame._add_polars_code`, `flowfile_frame/flowfile_frame/flow_frame.py:588`). The graph a frame builds is byte-identical in kind to what the Designer edits. - **Graph → visual editor**: `open_graph_in_editor(flow_graph)` at `flowfile/flowfile/api.py:399` saves the frame's graph and opens it in the running Designer. - **The parity discipline already exists**: the Descending-sort codegen bug (#544) was root-caused by *two* executors carrying separate direction parsers; the fix centralized it into `transform_schema.is_descending(...)` (`flowfile_core/flowfile_core/schemas/transform_schema.py:952`), used by **both** the code generator and the execution engine. And there is a real equivalence test: `flowfile_core/tests/flowfile/test_code_generator.py` parametrizes over `[export_flow_to_polars, export_flow_to_flowframe]` and asserts the generated code, when executed, yields a frame equal to `flow.get_node(N).get_resulting_data().data_frame`. So the pieces exist; what is *open* is **full round-trip idempotence with zero lossy fallbacks**. (Emission paths and the codegen bug family are owned by **flowfile-frame-and-codegen** and **flowfile-codegen-parity-campaign** — this pillar is the parity campaign's north star.) ### The two lossy escape hatches that break round-trip (know your enemy) 1. **`UnsupportedNodeError`** in the code generator — a node with no codegen handler. 2. **The base64-serialized-LazyFrame fallback** in `flow_frame.py` `_add_polars_code`: when an expression is not convertible to source (e.g. an unresolvable lambda), the frame serializes a LazyFrame blob into the node and logs a warning ending *"a breaking graph when using the the ui"* (sic — the doubled "the" is in the source; grep `breaking graph when using`, `flow_frame.py` ~line 655). Such a node does not round-trip and the visual graph no longer matches the computation. Any corpus flow that produces either is, by definition, not round-trippable. ### First three concrete steps (each PR-sized) 1. **Add a round-trip idempotence test.** Extend `flowfile_core/tests/flowfile/test_code_generator.py`: for a flow corpus, assert `export_flow_to_flowframe(g)` → re-imported through `flowfile_frame` → re-exported is **idempotent** (graph-shape or normalized-string stable), *and* both executions frame-equal. This surfaces the exact nodes that don't survive the trip. 2. **Fail the build on a lossy fallback.** Add a test/lint that fails when a corpus flow yields a base64-serialized-LazyFrame node or raises `UnsupportedNodeError` — converting "sometimes lossy" into a hard, counted gate. 3. **Add a native handler for the single most common `UnsupportedNodeError` node type** (in the converter + `code_generator/transform_handlers.py`), following the **one-shared-parser** rule (the `is_descending` pattern) so graph-exec and codegen can never diverge. Ship it with an equivalence test. Cross-ref flowfile-codegen-parity-campaign for the full bug-family playbook. ### Falsifiable milestone — you have a result when… For every flow in a fixed corpus: `export_flow_to_flowframe(g)` re-imported rebuilds `g'` such that `export_flow_to_flowframe(g') == export_flow_to_flowframe(g)` (idempotent), **both** `g` and `g'` execute to `assert_frame_equal` results, and the corpus produces **zero `UnsupportedNodeError` and zero base64-serialized-LazyFrame nodes**. Measurable check: the extended parametrized test is green with those three counters at zero. --- ## Pillar C — decentralized execution on a centralized catalog ### Why current SOTA fails Every catalog/lakehouse today assumes **central compute**: the catalog service *owns* the engine that reads and writes the data. "Decentralized" offerings still funnel queries through a shared cluster. Nobody offers: shared metadata + shared object-store data, where each participant brings **their own local compute** and treats a local Parquet file and a remote S3 Delta table as first-class peers in one pipeline, with credentials that never leak between participants. ### Flowfile's specific existing assets (VERIFIED, as of 2026-07-03 v0.12.7) Three seams are already in the right shape: - **Data can already live in object storage, per-catalog.** Migration **028** (`028_catalog_namespace_storage.py`) added `storage_uri` + `storage_connection_name` to level-0 (catalog-root) namespaces. `flowfile_core/flowfile_core/catalog/storage_backend.py` `resolve_for_namespace(...)` (line 123) resolves a cloud `CatalogStorageTarget` when a URI is set, resolving the `CloudStorageConnection` **as the catalog owner** (`owner_id = root.owner_id`, line 107) — never the calling user. `FLOWFILE_CATALOG_STORAGE_URI` / `_CONNECTION` remain a **creation-time default** for new catalogs (`catalog/services/namespaces.py:_env_default_storage`, line 148); `resolve_catalog_storage(_user_id, ...)` (line 137) is now a shim that **ignores its user_id**. (Env-var mechanics are owned by flowfile-config-and-flags.) - **The worker reads cloud tables with its own compute.** `flowfile_worker/catalog_reader.py` `open_catalog_table(...)` (line 29) does `pl.scan_delta(validate_catalog_uri(...), storage_options=...)` for cloud and local roots alike. Local compute reading remote data is already the code path. - **The catalog-DB seam is a single function.** `get_database_url()` at `shared/storage_config.py:402` is the one place every catalog-DB consumer resolves its URL (core `database/connection.py`, `database/migration.py`, `alembic/env.py`, the scheduler's `engine.py`, `shared/run_completion.py`). Today it only ever emits `sqlite:///…`. - **Secrets are already instance-independent.** A stored secret is `$ffsec$1$$` — the worker (and any peer) re-derives the per-user key from the shared master key **with zero core round-trip** (`secret_manager.py` ↔ `flowfile_worker/secrets.py`, byte-for-byte parallel). Sharing/decentralizing does **not** require re-encryption. (This contract is owned by flowfile-architecture-contract.) Shipped (#738): `FLOWFILE_DATABASE_URL`/`FLOWFILE_DB_PATH` accept full SQLAlchemy URLs and `_catalog_db_exists` is Alembic-based for server URLs. What is **open**: no two-instance harmony test. The metadata DB is deliberately always-local today; migration 028 is forward-only with no backfill. Also open: **global components** — user-defined components today are per-instance `.py` files under `/user_defined_nodes/` (`shared/storage_config.py:109-114`, loaded into `CUSTOM_NODE_STORE`); sharing them through the central catalog is a candidate follow-on to steps 1–3 below, with no design yet. ### PostgreSQL catalog support — shipped (#738) The catalog runs on PostgreSQL 16 via `FLOWFILE_DATABASE_URL`; `test-catalog-databases.yml` gates it. ### The one security wrinkle you must respect A serialized cloud-scan LazyFrame blob carries its source's **credentials, frozen at build time**: S3/Azure keys as an `EncryptedCredentialProvider` `$ffsec$` ciphertext (decryptable by any process holding the master key), ADLS service-principal secrets and GCS keys still inline in `storage_options`. `serialized_frame_uses_cloud(blob)` (`storage_backend.py:31`) exists precisely to catch this: a cloud-scan blob **must never be replayed** — re-run the producer instead. A decentralized catalog makes this sharper: a replayed blob would ship one instance's credentials to another. Any cross-instance data path must route through `resolve_for_namespace` (owner-keyed credentials, worker re-derives), never through a shipped blob. ### First three concrete steps (each PR-sized) 1. **Generalize the metadata seam.** The exact worklist is known (see the probe above): (a) teach `get_database_url()` (`shared/storage_config.py:402`) to pass `://`-bearing values through as full URLs; (b) make `_catalog_db_exists()` (`database/migration.py:84`) `alembic_version`-based for server URLs; (c) fix the four migration files (002, 016, 026, 020) with SQLite-identical replacements (`sa.false()`/`sa.true()` for the defaults, `TRUE/FALSE` literals or boolean-typed bindparams for the raw SQL, portable types in 026's `_wp_new` DDL); (d) add a `docker_integration`-marked test that boots Postgres (testcontainers or `test_utils/postgres/`) and runs `run_alembic_upgrade()` against it. Expected observations: before the fixes the chain dies at 002 with `psycopg2.errors.DatatypeMismatch` on `is_optimized`; after, `SELECT version_num FROM alembic_version` returns `028` and the SQLite before/after `.schema` diff is empty. Keep `render_as_batch=True` — it is a no-op off SQLite. (Migration mechanics: flowfile-testing-and-validation + flowfile-change-control.) 2. **Prove two-instance data sharing on the already-built cloud path.** Add an integration test: two core processes sharing one catalog DB (via step 1's Postgres URL) and one MinIO bucket via a per-namespace `storage_uri`/`storage_connection_name` (migration 028). Assert instance B **sees** and **reads** a table instance A wrote, using instance B's **own** worker. (MinIO is already a `test_utils/` fixture.) 3. **Mixed local + cloud join, credential-blob guard hardened.** Add a flow that joins a local Parquet file with a remote S3 Delta catalog table in one graph, and add an assertion that no cloud-backed catalog read is ever served from a replayed blob across instances (`serialized_frame_uses_cloud` must gate it). Cross-ref flowfile-architecture-contract for the worker-re-derives-secrets contract. ### Falsifiable milestone — you have a result when… Two independent local Flowfile instances (each with its own worker/compute), pointed at one PostgreSQL catalog and one S3 data root, can both **list, read, and write** catalog tables; a single flow **joins a local file with a remote S3 table** and produces a correct frame; and **neither instance's worker ever holds the other's credentials in a replayable form**. Measurable check: an integration test with two core processes sharing a Postgres `get_database_url` + one MinIO bucket, asserting cross-instance visibility, a mixed local+cloud join result, and zero cloud-blob replays. --- ## Research methodology — hunch → merged change (or documented retirement) The frontier is speculative; the *discipline* is not. A change earns its way in here the same way every merged change does. Follow this or your spike stays a spike. ### 1. Predict numbers before you run State the hypothesis as a **quantity with a direction and a threshold**, written down *before* the experiment: "the executor coercion in Pillar A/step 3 lifts benchmark success from X% to ≥Y%," or "the native handler in Pillar B/step 3 drops corpus `UnsupportedNodeError` count from N to 0." A hypothesis that can only be evaluated *after* seeing the output is not a hypothesis. Each pillar's 1. **Generalize the metadata seam — shipped (#738).** Server URLs pass through `get_database_url()`, `_catalog_db_exists()` is Alembic-based for them, and migrations 002/016/020/026 are dialect-portable. (response-queue theft between two parents sharing a key) explained *every* symptom, not just the common one. ### 3. Survive an assigned adversarial-refutation pass Before proposing the change, have someone (or yourself, in a separate pass) **try to kill it**: find the input that breaks the number, the confound that explains the delta without your mechanism, the negative case you didn't run. This repo has a documented failure mode for skipping this — Windows E2E tests that were green while testing nothing (OR-instead-of-AND on startup signals, `test.skip()` on launch failure, error-swallowing `.catch(() => false)`). A result that hasn't survived an attempt to refute it is that green-but-empty test. ### 4. The idea lifecycle (where the gates live) ``` hunch └─► experiment flag (MutableBool / env gate — see flowfile-config-and-flags) default OFF; ship the plumbing dark so it can't regress the default path └─► validation (the evidence bar — see flowfile-testing-and-validation) real integration tests over mocks; the pre-registered number must clear its threshold; survives the adversarial pass └─► EITHER adopted change (through flowfile-change-control: PR, drift gates, review) OR documented retirement (write it up in flowfile-failure-archaeology so the next person doesn't re-run the dead experiment) ``` A retired idea is a *deliverable*, not a failure — the repo's abandoned directions (Airbyte connector, in-house fuzzy matching extracted to an external package, the walked-back FastAPI upgrade, the whole Electron toolchain) are load-bearing knowledge. Retire loudly. ### 5. Where good ideas have actually come from here Not from greenfield brainstorming — from **user pain and production bugs**. The entire Alembic migration system was born from a production incident: a run-type mismatch between local and Docker databases (fixed by migration `006_normalize_run_type.py`) forced the maintainer to "implement a mechanism to downgrade the db," which became versioned migrations. The uuid columns (`flow_uuid`, viz/dashboard uuids) trace to a concrete SQLite rowid-reuse bug pulling one flow's runs into another's history. The DST-correct cron cursor traces to a real double-fire risk. **The best frontier work starts from an observed failure, then generalizes** — when scoping a pillar, look first for the smallest real pain it removes, and make *that* your first PR. --- ## Provenance and maintenance Re-verify volatile facts before relying on them (all commands read-only, run from repo root). Facts are stamped as of **2026-07-03 (v0.12.7)**. | Claim | One-line re-verification | |---|---| | Planner package + `mode="stage"` staging exists | `ls flowfile_core/flowfile_core/ai/agents/planner/ && grep -n 'mode: ExecutionMode' flowfile_core/flowfile_core/ai/tools/executor/dispatch.py` | | `bundle_staged_results` → `GraphDiff` | `grep -n 'def bundle_staged_results' flowfile_core/flowfile_core/ai/diff.py` | | Executor coercion + refusal seams | `ls flowfile_core/flowfile_core/ai/tools/executor/ ` (expect `coercions.py`, `refusals.py`, `dispatch.py`) | | Graph→code exporters | `grep -n 'def export_flow_to_polars\|def export_flow_to_flowframe' flowfile_core/flowfile_core/flowfile/code_generator/code_generator.py` | | Code→graph frame bridge | `grep -n 'def _add_polars_code' flowfile_frame/flowfile_frame/flow_frame.py` | | Graph→editor entry point | `grep -n 'def open_graph_in_editor' flowfile/flowfile/api.py` | | One shared sort-direction parser (parity discipline) | `grep -n 'def is_descending' flowfile_core/flowfile_core/schemas/transform_schema.py` | | Codegen equivalence test parametrized over both exporters | `grep -n 'export_flow_to_polars, export_flow_to_flowframe' flowfile_core/tests/flowfile/test_code_generator.py` | | Per-catalog object storage (migration 028) | `ls flowfile_core/flowfile_core/alembic/versions/ | grep 028` | | `resolve_for_namespace` / owner-keyed creds / `resolve_catalog_storage` shim | `grep -n 'def resolve_for_namespace\|owner_id = root.owner_id\|def resolve_catalog_storage' flowfile_core/flowfile_core/catalog/storage_backend.py` | | Env vars are creation-time default only | `grep -n 'FLOWFILE_CATALOG_STORAGE_URI\|FLOWFILE_CATALOG_STORAGE_CONNECTION' flowfile_core/flowfile_core/configs/settings.py` | | Worker reads cloud catalog tables with its own compute | `grep -n 'def open_catalog_table' flowfile_worker/flowfile_worker/catalog_reader.py` | | `get_database_url` is the single catalog-DB seam (`FLOWFILE_DATABASE_URL` first) | `grep -n 'def get_database_url' -A12 shared/storage_config.py` | | Engine creators already dialect-gate `check_same_thread` | `grep -rn 'check_same_thread' flowfile_core/flowfile_core/database/connection.py flowfile_scheduler/flowfile_scheduler/engine.py shared/run_completion.py` | | Postgres driver is dev-group only; test fixture exists | `grep -n 'psycopg2-binary\|start_postgres' pyproject.toml && ls test_utils/postgres/` | | Components are per-instance files (global components open) | `grep -n 'def user_defined_nodes_directory' shared/storage_config.py` | | Secrets worker-re-derivable format | `grep -n 'SECRET_FORMAT_PREFIX\|KEY_DERIVATION_VERSION' flowfile_core/flowfile_core/secret_manager/secret_manager.py flowfile_worker/flowfile_worker/secrets.py` | | Cloud-blob replay guard | `grep -n 'def serialized_frame_uses_cloud' flowfile_core/flowfile_core/catalog/storage_backend.py` | | Migration born from a production bug (methodology §5) | `head -20 flowfile_core/flowfile_core/alembic/versions/006_normalize_run_type.py` | **Candidate / pending direction (do not treat as current truth):** an unmerged architecture branch introduces an `ExecutionBackend` seam (local vs worker compute), a `WorkerTransport` owner of worker URLs, and a `NodeSpec` registry with a declarative `_add_from_spec` node path. As of 2026-07-03 these names return **nothing** on `main` (`grep -rl 'class ExecutionBackend\|class WorkerTransport\|class NodeSpec' flowfile_core/flowfile_core` is empty); re-run that grep before citing them, and check whether the branch has merged — if it has, it reshapes how nodes and execution are added and Pillars A/C should be re-scoped against it. **When the frontier moves:** if a pillar ships, demote it out of this skill into the owning sibling (A→flowfile-ai-subsystem, B→flowfile-codegen-parity-campaign, C→flowfile-architecture-contract) and replace it here with the *next* open problem. Keep this skill about what is unbuilt.